C++17 In Detail

31 August 2020

6 Efficient Things You Can Do to Refactor a C++ Project

C++ Refactor Techniques

I 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 in 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:

1. Update the Compiler and Set Correct C++ Standard Conformance

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 - use bind, 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:

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:

void Render(const CViArray<float>& numbers, IRenderer* renderer);

And a simple unit/component test:

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:

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):

class DataRenderer {
public:
    void Reset();
    void Render(const CViArray<float>& numbers, AVSystem* avSystem);
private:
    // ..
};

And now rather than calling:

array.Render(avSystem);

I have to write:

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:

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:

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.

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:

CAlgManager g_algManager;
CAVSystem g_avSystem;

And after:

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.

If you like my work and you want to get extra C++ content, exlusive articles and weekly curated news, then check out my Patreon website:

© 2017, Bartlomiej Filipek, Blogger platform
Disclaimer: Any opinions expressed herein are in no way representative of those of my employers. All data and information provided on this site is for informational purposes only. I try to write complete and accurate articles, but the web-site will not be liable for any errors, omissions, or delays in this information or any losses, injuries, or damages arising from its display or use.
This site contains ads or referral links, which provide me with a commission. Thank you for your understanding.