6 Efficient Things You Can Do to Refactor a C++ Project
Join the DZone community and get the full member experience.
Join For FreeI took my old pet project from 2006, experimented, refactored it, and made it more "modern C++". Here are my lessons and six practical steps that you can apply to your projects.
Let's start.
Background and Test Project
All changes that I describe here are based on my experience with a pet project which I dig out from the studies. It's an application that visualises sorting algorithms. I wrote it in 2005/2006 and used C++98/03, Win32Api and OpenGL, all created in Visual Studio (probably 2003 if I remember :).
Here's the app preview:
Above you can see a cool animation of quick sort algorithm. The algorithm works on an array of values (can be randomised, sorted, reverse sorted, etc.) and performs a single step of the algorithm around 30 times per second. The input data is then taken and drawn as a diagram with some reflection underneath. The green element is the currently accessed value, and the light-blue section represents the part of the array that the algorithm is working on.
While the app looks nice, It has some awful ideas in the code... so why not improve it and experiment.
Here's the GitHub repo: github/fenbf/ViAlg-Update
Let's start with the first step:
Staying with GCC 3.0 is not helpful when GCC 10 is ready :)
Working in Visual Studio 2008 is not the best idea when VS 2019 is out there and stable :)
If you can, and your company policy allows that, and there are resources, then upgrade the compiler to the latest version you can get. Not only will you have a chance to leverage the latest C++ features, but also the compiler will have a lot of bugs fixed. Having regular updates can make your projects safer and more stable.
From my perspective, it's also good to update toolchains frequently. That way it's easier to fix broken code and have a smoother transition. If you update once per 5... 7 years then such a task seems to be "huge", and it's delayed and delayed.
Another topic is that when you have the compiler please remember about setting the correct C++ version!
You can use the latest VS 2019 and still compiler with C++11 flag, or C++14 (that might be beneficial, as the compiler bugs will be solved and you can enjoy the latest IDE functionalities). This will be also easier for you to upgrade to the C++17 standard once you have the process working.
You can, of course, go further than that and also update or get the best tools you can get for C++: most recent IDE, build systems, integrations, reviewing tools, etc, etc... but that's a story for a separate and long article :) I mentioned some techniques with tooling in my previous article: "Use the Force, Luke"... or Modern C++ Tools, so you may want to check it out as well.
2. Fix Code With Deprecated or Removed C++ Features
Once you have the compiler and the C++ version set you can fix some broken code or improve things that were deprecated in C++.
Here are some of the items that you might consider:
auto_ptr
deprecated in C++11 and removed in C++17- functional stuff like
bind1st
,bind2nd
, etc - usebind
,bind_front
or lambdas - dynamic exception specification, deprecated in C++11 and removed in C++17
- the
register
keyword, removed in C++17 random_shuffle
, deprecated since C++11 and removed in C++17- trigraphs removed in C++17
- and many more
Your compiler can warn you about those features, and you can even use some extra tools like clang-tidy to modernise some code automatically. For example, try modernise_auto_ptr
which can fix auto_ptr
usage in your code. See more on my blog C++17 in details: fixes and deprecation - auto_ptr
And also here are the lists of removed/deprecated features between C++ versions:
3. Start Adding Unit Tests
That's a game-changer!
Not only unit tests allow me to be more confident about the code, but it also forces me to improve the code.
One handy part?
Making thing to compile without bringing all dependencies
For example I had the DataRendered
class:
x
class DataRenderer {
public:
void Reset();
void Render(const CViArray<float>& numbers, AVSystem* avSystem);
private:
// ..
};
The renderer knows how to render array with numbers using the AVSystem
. The problem is that AVSystem
is a class which makes calls to OpenGL and it's not easy to test. To make the whole test usable, I decided to extract the interface from the AVSystem
- it's called IRenderer
. That way I can provide a test rendering system, and I can compile my test suite without any OpenGL function calls.
The new declaration of the DataRenderer::Render
member function:
xxxxxxxxxx
void Render(const CViArray<float>& numbers, IRenderer* renderer);
And a simple unit/component test:
x
TEST(Decoupling, Rendering) {
TestLogger testLogger;
CAlgManager mgr(testLogger);
TestRenderer testRenderer;
constexpr size_t NumElements = 100;
mgr.SetNumOfElements(NumElements);
mgr.GenerateData(DataOrder::doSpecialRandomized);
mgr.SetAlgorithm(ID_METHOD_QUICKSORT);
mgr.Render(&testRenderer);
EXPECT_EQ(testRenderer.numDrawCalls, NumElements);
}
With TestRenderer
(it only has a counter for the draw calls) I can test if the whole thing is compiling and working as expected, without any burden from handling or mocking OpenGL. We'll continue with that topic later, see the 4th point.
If you use Visual Studio, you can use various testing frameworks, for example, here's some documentation:
4. Decouple or Extract Classes
While unit tests can expose some issues with coupling and interfaces, sometimes types simply look wrong. Have a look at the following class:
xxxxxxxxxx
template <class T>
class CViArray {
public:
CViArray(int iSize);
CViArray(): m_iLast(-1), m_iLast2(-1), m_iL(-1), m_iR(-1) { }
~CViArray();
void Render(CAVSystem *avSystem);
void Generate(DataOrder dOrder);
void Resize(int iSize);
void SetSection(int iLeft, int iRight);
void SetAdditionalMark(int iId);
int GetSize()
const T& operator [] (int iId) const;
T& operator [] (int iId);
private:
std::vector<T> m_vArray;
std::vector<T> m_vCurrPos; // for animation
int m_iLast; // last accessed element
int m_iLast2; // additional accesed element
int m_iL, m_iR; // highlighted section - left and right
static constexpr float s_AnimBlendFactor = 0.1f;
};
As you can see ViArray
tries to wrap a standard vector plus add some extra capabilities that can be used for Algorithm implementations.
But do we really have to have rendering code inside this class? That's not the best place.
We can extract the rendering part into a separate type (you've actually seen it in the 3rd point):
x
class DataRenderer {
public:
void Reset();
void Render(const CViArray<float>& numbers, AVSystem* avSystem);
private:
// ..
};
And now rather than calling:
xxxxxxxxxx
array.Render(avSystem);
I have to write:
xxxxxxxxxx
renderer.Render(array, avSystem);
Much better!
Here are some benefits of the new design:
- It's extensible, easy to add new rendering features that won't spoil the array interface.
ViArray
is focusing only on the things that are related to data/element processing.- You can use
ViArray
in situations when you don't need to render anything
We can also go further than that, see the next step:
5. Extract Non-member Functions
In the previous step you saw how I extracter Render method into a separate class... but there is still a suspicious code there:
xxxxxxxxxx
template <class T>
class CViArray {
public:
CViArray(int iSize);
CViArray(): m_iLast(-1), m_iLast2(-1), m_iL(-1), m_iR(-1) { }
~CViArray();
void Generate(DataOrder dOrder);
// ...
Should the Generate
function be inside this class?
It could be better if that's a non-member function, similar to algorithms that we have in the Standard Library.
Let's move the code out of that class:
xxxxxxxxxx
template<typename T>
void GenerateData(std::vector<T>& outVec, DataOrder dOrder) {
switch (dOrder) {
// implement...
}
}
It's still not the best approach; I could probably use iterators here so it can support various containers. But this can be the next step for refactoring and for now it's good enough.
All in all, after a few refactoring iterations, the ViArray
class looks much better.
But it's not all, how about looking at global state?
6. Reduce the Global State
Loggers... they are handy but how to make them available for all compilation units and objects?
How about making them global?
Yes :)
While this was my first solution, back in 2006, in the newest version of the application, I refactored it, and now logger is just an object defined in main()
and then passed to objects that need it.
x
int WINAPI WinMain(HINSTANCE, HINSTANCE, LPSTR, int) {
CLog logger{ "log.html" };
AppState appState{ logger };
InitApp(logger, appState);
// ...
}
And another topic: Do you see that AppState
class? It's a class that wraps a two "managers" that were globals:
Before:
x
CAlgManager g_algManager;
CAVSystem g_avSystem;
And after:
x
struct AppState {
explicit AppState(const CLog& logger);
CAlgManager m_algManager;
CAVSystem m_avSystem;
};
AppState::AppState(const CLog& logger) :
m_algManager { logger},
m_avSystem { logger}
{
// init code...
}
And an object of the AppState type is defined inside main()
.
What are the benefits?
- better control over the lifetime of the objects
- it's important when I want to log something in destruction, so I need to make sure loggers are destroyed last
- extracted initialisation code from one large
Init()
function
I have still some other globals that I plan to convert, so it's work in progress.
Summary
In the article, you've seen several techniques you can use to make your code a bit better. We covered updating compilers and toolchains, decoupling code, using unit tests, handling global state.
I should probably mention another point: Having Fun :)
If you such refactoring on production then maybe it's good to keep balance, but if you have a please to refactor your pet project... then why not experiment. Try new features, patters. This can teach you a lot.
And by the way: For my Patrons I also prepared an extended version of this article, it includes two more bullet points (about Keeping It Simple and Tooling), have a look: at the Patreon Page and join :)
Back To you
The techniques that I presented in the article are not carved in stone and bulletproof... I wonder what your techniques with legacy code are? Please share your comments below the article.
Published at DZone with permission of Bartłomiej Filipek, DZone MVB. See the original article here.
Opinions expressed by DZone contributors are their own.
Comments