Every refactoring can be composed of a set of simple basic steps. Knowing these basic refactoring steps is crucial when we want to continuously compile and test during the refactoring.
With this post, I will start a list of the basic refactoring steps. Modern C++ IDEs sometimes contain refactoring tooling that supports some of the basic steps, but not all. For many of the steps, there are some tips and tricks how we can perform them manually with the help of our compiler or only a text editor.
Names are often hard to do right on the first try. Therefore, changing names to something better and more descriptive is an important and probably the most frequently used refactoring step. Depending on what it is we want to rename, it can be more or less complicated to do.
The basic approach for renaming is to simply let your editor search and replace the old name with the new one. Make sure that you search case sensitive and replace whole words only.
- Names with constrained scope: This applies mostly to local variables, but also to private class members, helper functions that are only known inside one translation unit etc. Simply constrain the search and replace to the scope, e.g. to a single source file or a single function. Sometimes we have the same name for more than one variable. However, in such cases, we often want to rename both occurrences.
- Types, public member variables etc.: For names that are globally accessible search and replace can have some risk. For example, if the name is very generic it is likely to occur more than once. A simple search and replace over the whole code base will then change not only one, but multiple names. If you want to avoid that, the compiler can help: rename the declarations/definitions of the entity, and the compiler will spit out the references to the old name as error messages. In rare cases, it may not complain, e.g. when lookup rules find another entity with the original name that was previously hidden.
- Functions: Renaming functions is the most tricky part. Functions can be overloaded, and you may or may not want to rename the overloads as well. Also, the trick of simply renaming the declarations may not work, because, in the presence of overloads, the risk is high that the compiler simply will find another function to call. In addition, renaming virtual functions in a class hierarchy may subtly break the behavior unless we consequently use
An approach to the problem can be to search for all occurrences of the function name in headers and rename all of them to two different names, e.g. rename
TEMP_should_stay_foo. Then the compiler will again point us to every function call and we can decide for each case which of the two names should be called. In the end, a global search and replace can be used to rename
It often improves maintainability to extract expressions into their own variables, a bunch of statements into their own function, or a complete set of functionality into its own class.
- Extract variable: This is pretty straightforward: define a variable of the desired type and initialize it with the expression you want to replace. Then search and that exact expression and replace it with the variable. Caution: replacing multiple occurrences of an expression with side effects with a variable may change your program behavior.
- Extract function: Take some lines of code, e.g. the body of a loop, and put them into their own function. The challenge here is to determine return values and parameters, and especially whether arguments are taken by value or by reference. I have written an example in a previous post on how to leverage the compiler to extract a function.
- Extracting a class technically is not one of the basic refactoring steps, as it consists of single steps itself: Usually, I’d start by wrapping one or more variables into a struct that will become the new class eventually. The next step is to take functions that work on the struct’s members and move them into the struct. Accesses to variables outside the struct will need to either be encapsulated in accessor functions or given to the function explicitly as parameters. You usually will need to extract the new member function first from the scope it will be called from later.
- Extract parameter: Sometimes you will have a function that almost does what you need elsewhere but lacks some generality. What we usually want to do then is parametrizing it with the part that was fixed until now and needs to ve variable. There are two ways to do this: Either copy the function for now and parametrize the copy, making it an overload of the old version. This allows us to replace the calls to the old version one after another over time, but we will have duplicated code until we are finished. The other option is to add the parameter but give it a default that corresponds to the old version. After that, we can modify the call sites to explicitly provide the new parameter, and remove the default in the end.
There are more extraction steps. For example, CLion offers “extract constant” and “extract define” which are similar to extract variable, and extract typedef which basically does the same for type expressions. They also have “extract superclass” and “extract subclass” which allow you to define a base class or derived class and move some of your class members into it in one single step.
Inlining is the exact opposite of extracting: Replace each occurrence of a variable or constant with the expression it is initialized with, or replace a function call with the function body. This can come handy if used together with the other basic refactoring steps. For example, if you want to replace a single function with three other functions that do the same work together, you can first extract the three new functions from the old one, then inline the old function since it only contains the three function calls.
These were, in my eyes, the most important and basic refactoring steps. There are a few more, but not many more. I can see that this post is very theoretical and lacks examples, but it is long enough as it is.