DEV Community

loading...
Cover image for Modern C++: Safety and Expressiveness with override and final

Modern C++: Safety and Expressiveness with override and final

fenbf profile image Bartlomiej Filipek ・7 min read

While C++11 is with us for a decade now, it's good to go back and recall some of its best features. Today I'd like to consider override and final keywords which add a crucial safety when you build class hierarchies with lots of virtual member functions.

See how to prevent common bugs, and how to leverage tools to make your code safer.

Initially published at CppStories

An Unexpected Code Path Errors

Can you spot an error in the following code?

There's a base class - BasePacket and a single derived class - NetworkPacket:

class BasePacket {
public:
    virtual ~BasePacket() = default;

    virtual bool Generate() = 0;
    virtual bool Verify(std::string_view ) { return true; }
};

class NetworkPacket : public BasePacket {
public:
    NetworkPacket() = default;

    bool Generate() { return true; }

    bool Verify(std::string_view config) const {
        std::cout << "verifying against: " << config;
        return true;
    }

private:
    std::any data_;
};
Enter fullscreen mode Exit fullscreen mode

And then we have a simple use case. We'd like to call the Verify function using a pointer to the base class:

int main() {
    std::unique_ptr<BasePacket> pPacket = std::make_unique<NetworkPacket>();
    pPacket->Verify("test cfg: length: 123: https: false");
}
Enter fullscreen mode Exit fullscreen mode

Do you know what's the output here? Give it a try and think a minute.

.

.

.

.

Here's the output:


Enter fullscreen mode Exit fullscreen mode

Yep, it's an empty line. There's no sensible output as our derived Verify function from NetworkPacket wasn't called at all!

The reason?

As you can see, we have two different function declaration:

bool NetworkPacket::Verify(std::string_view config) const;
Enter fullscreen mode Exit fullscreen mode

And

virtual bool BasePacket::Verify(std::string_view config);
Enter fullscreen mode Exit fullscreen mode

Since they don't match the compiler can call only the base class's function (as we call it through a pointer to the base class). The function from NetworkPacket is not available for the overload resolution at this stage.

We can imagine that one developer created the base class, another developer wrote the NetworkPacket and wanted to narrow the contract of this particular function and make it const.

In our example we have a mismatch on const, but it can happen also with parameter types:

bool NetworkPacket::Verify(std::string_view config, int arg) const;
// vs
virtual bool BasePacket::Verify(std::string_view config, double arg) const;
Enter fullscreen mode Exit fullscreen mode

See the code @Compiler Explorer

A Complex Case With #define

There's even more fun! See this example:

In one article @PVS-Studio blog there's an interesting case where functions match in 32-bit compilation mode, but when you change to 64-bit, then it fails. Have a look at this synthesised example:

//#define WIN64 // uncomment later...

typedef uint32_t DWORD;

#ifdef WIN64
typedef uint64_t DWORD_PTR;
#else
typedef DWORD DWORD_PTR;
#endif

struct Base {
    virtual int execute(DWORD_PTR dwData) { return 1; };
};

struct Derived : public Base {
    int execute(DWORD dwData) { return 2; }; 
};

int run(Base& b) { return b.execute(0); }

int main() {
    Derived d;
    return run(d);
}
Enter fullscreen mode Exit fullscreen mode

As you can see above, there's a mismatch in the function declarations. This example is based on a real use case in some WinApi code! The code works nicely in 32 bits when DWORD and DWORD_PTR matches and both mean uint32_t. However, when you define WIN64 then things came apart and fail.

See the example @Compiler Explorer. Have a look at the program's output, in one case it's 1, and in the second case it's 2.

See more in Lesson 12. Pattern 4. Virtual functions @PVS-Studio Blog.

Risks - Sum Up

What do we risk when the virtual functions don't match?

  • Wrong code path might be executed. This case is particularly scary when you have large hierarchies with complex code; some function may call other base functions, so deducing what's wrong might not be an easy debugging task.
  • Hard to read code. Sometimes it's not clear if a function overrides a virtual one from the base class or not. Having a separate keyword makes it visible and explicit.

The Solution - Apply override

Before C++11, it was quite common to have those kinds of errors and misuses. Such bugs were also quite hard to spot early on. Fortunately, following the path of other programming languages like Java or C# Modern C++ gave us a handy keyword override.

In C++ we should make a habit of marking every function which overrides with the override contextual keyword. Then the compiler knows the expected results and can report an error. In our case when I add override to the NetworkPacket implementation:

bool Verify(std::string_view config) const override {
    std::cout << "verifying against: " << config;
    return true;
}
Enter fullscreen mode Exit fullscreen mode

I'll immediately get a compiler error:

 error: 'bool NetworkPacket::Verify(std::string_view) const' marked 'override', but does not override
   21 |  bool Verify(std::string_view config) const override {
      |       ^~~~~~
Enter fullscreen mode Exit fullscreen mode

This is much better than getting the wrong path execution after a few days :)

Same happens for our WIN64 example. When you apply override you'll get a nice warning:

error: 'int Derived::execute(DWORD)' marked 'override', but does not override
Enter fullscreen mode Exit fullscreen mode

See the improved code @Compiler Explorer.

Additionally, there's also a "reverse" situation:

What if our base class designer forgot to make a function virtual? Then we can expect a similar error.

In both situations, we have to go back and compare the declarations and see what's wrong.

The override keyword also reduces the need to write virtual in every possible place.

struct Base {
    virtual void execute() = 0;
};

struct Derived : public Base {
    virtual void execute() { }; // virtual not needed
};
Enter fullscreen mode Exit fullscreen mode

Before C++11, it was common to put virtual to mark that this function is overriding, but only the top-most functions in the base class need such a declaration. It's much better to use override:

struct AnotherDerived : public Base {
    void execute() override { }; // better!
};
Enter fullscreen mode Exit fullscreen mode

Guidelines

Let's also have a look at Core Guidelines: We have a separate topic on override:

C.128: Virtual functions should specify exactly one of virtual, override, or final - link

We can read in the guideline with override we aim to address the following issues:

  • implicit virtual - you wanted (or didn't wish to) a function to be virtual, but due to some subtle differences with the declaration it isn't (or is).
  • implicit override - you wanted (or didn't want) a function to be an override, but it appears to be the opposite way.

We can also have a look at Google C++ Style Guide where we can find:

Explicitly annotate overrides of virtual functions or virtual destructors with exactly one of an override or (less frequently) final specifier. Do not use virtual when declaring an override....

Adding final

If you want to block the possibility to override then C++11 also brings another keyword final. See the example below:

struct Base {
    virtual void doStuff() final;
};

struct Derived : public Base {
    void doStuff(); 
};
Enter fullscreen mode Exit fullscreen mode

And Clang reports:

<source>:6:10: error: virtual function 'virtual void Derived::doStuff()' overriding final function
    6 |     void doStuff();
      |          ^~~~~~~
Enter fullscreen mode Exit fullscreen mode

See here @CompilerExplorer

It's also not a problem to mix override with final (although it's harder to read and probably uncommon):

struct Base {
    virtual void doStuff();
};

struct Derived : public Base {
    void doStuff() override final; 
};

struct ExDerived : public Derived {
    void doStuff() override; 
};
Enter fullscreen mode Exit fullscreen mode

This time, we allow to override in one base class, but then we block this possibility later in the hierarchy.

It also appears that the final keyword can be used to ensure your functions are properly marked with override.

Have a look at this response by Howard Hinnant:

c++ - Is there any sense in marking a base class function as both virtual and final? - Stack Overflow

I marked the virtual function in the base class with final, and the compiler quickly showed me where every single override was declared. It was then very easy to decorate the overrides how I wanted, and remove the final from the virtual in the base class.

Another interesting use case is with giving the compiler more ways to devirtualise function calls.

See a separate blog post on that in the MSVC Team blog: The Performance Benefits of Final Classes | C++ Team Blog.

Tools

After the standardisation of C++11, many useful tools started to appear and catch up with the Standard. One of the best and free tools is clang-tidy which offers help with code modernisation.

Usually when you forget to apply override the compiler can do nothing about it and won't report any errors.

We can enable clang-tidy in Compiler Explorer and if we pass the following command:

--checks='modernize-use-override'
Enter fullscreen mode Exit fullscreen mode

We will get the following report:

<source>:19:7: warning: annotate this function with 'override' 
               or (rarely) 'final' [modernize-use-override]
        bool Generate() { return true; }
             ^
            override
<source>:21:7: warning: annotate this function with 'override' 
               or (rarely) 'final' [modernize-use-override]
        bool Verify(std::string_view config) {
             ^
            override
Enter fullscreen mode Exit fullscreen mode

Here's the configured Compiler Explorer output: https://godbolt.org/z/jafxTn and the screenshot:

And here's the list of all checks available in Clang Tidy. You can experiment and find some other suggestions from the tool.

If you want to read more you can also have a look at my separate guest post on Clang-Tidy: A Brief Introduction To Clang-Tidy And Its Role in Visual Assist – Tomato Soup.

Summary

The override keyword is very simple to use and makes your code more expressive and more straightforward to read. There's no downside of using it and, as you could see in one example, without it we sometimes risk some unwanted code path to be executed!

For completeness, you can also leverage final to have more control over the virtual functions and permissions which classes can or shouldn't override functions.

We also looked at a popular and easy-to-use tool clang-tidy that can help us automate the process of modernising code bases.

Your Turn

  • What's your experience with override? Do you use it? Is that your habit?
  • Have you tried final? I'm interested in some good use cases for this feature.

More from the Author

Bartek is author of two books - "C++17 In Detail" and "C++ Lambda Story" - both available @Leanpub - Learn Modern C++ in a practical way.

Discussion (0)

Forem Open with the Forem app