C++ advanced topics for experienced C++ devs

Amir Kirsh
Amir Kirsh reading time: 11 minutes
April 4, 2022

C++ is moving forward, fast, and it’s not easy to keep pace. We’ve already covered this in previous posts, discussing the evolution of C++ and how to modernize legacy C++ code. In this post we want to focus on a list of advanced topics that experienced C++ developers can keep up to date with. The list is not exhaustive, and it is a bit subjective (we may have waived some item that you would think is actually very important, or included something you see as not so crucial for you). And though we aim for a list of advanced C++ topics, some topics that one person might consider advanced might be basic for another. Also, there are different levels of complexity in new C++ features. Some are for everyone while others are for library and infra maintainers. We will try to cover the things we see as relevant, without limiting ourselves to a specific C++ usage. One last note before we begin: advanced C++ stuff doesn’t necessarily mean new C++ features. There are advanced C++ topics that should get their proper attention and are with us already from C++98, and we will have some of those in the list as well.

It is important to note that this post is not a tutorial, it is not aimed at teaching things but rather at pointing out relevant advanced C++ topics that you should put into your list of required C++ skills and add to your C++ learning path.

Let’s start.

Templates

Templates are some of the strongest tools C++ offers, and in some cases they’re not being utilized as they should be. In some companies templates are considered the infra team’s tool, while the “regular” C++ teams are just using them. I believe that advanced C++ developers from any team should be capable of implementing templates wherever relevant to allow code reuse, more efficient code and better APIs.

You don’t have to know all the specifics of templates (unless you do write a generic template library) but you should start with mastering the details of simple template functions and template classes, then the rules of type and non-type template parameters (and you may put aside template template arguments, at least for a start).

C++11 added variadic templates, that you should also know how to use for your needs. Remember that functions such as emplace, make_tuple, make_unique and make_shared all rely on variadic templates, and it is not theoretical that you may need to implement a similar factory method by yourself using variadic templates.

You could also use template specialization, a technique that stretches back to C++98. Be it full specialization or partial specialization, you may find this technique useful for implementing a more efficient algorithm for a specific type or family of types.

Another old (again, C++98 based) but useful template technique is tag dispatching. This nice post shows how C++20 concepts may be used instead of tag dispatching. Additionally, the C++17 if-constexpr can sometimes be a relevant replacement for tag dispatch.

C++17 added Class Template Argument Deduction (CTAD) which makes it easier to instantiate template class objects without the need to provide template arguments, like in:

std::vector v1{1, 2, 3}; // std::vector<int>
std::tuple p1{1, "wahad", "one"s}; //std::tuple<int, const char*, std::string>

The term static polymorphism is also important. When you know at compile time if certain code should be using TCPConnection or UDPConnection, using templates properly  to manage the different implementations would result in better runtime performance compared to dynamic polymorphism, which is based on virtual functions.

To conclude our list of template techniques let’s add CRTP, which is used quite often – but not exclusively – as the tool behind static polymorphism (see this example for implementing a clone method in a base class).

Note that SFINAE is not listed here, as while it was quite relevant until C++20, it’s now being replaced with concepts that make it look obsolete.

Recommended reading:

Template exercise:

Suppose that we need to manage a boolean array for which most entries (99%) are false. Implement a template-based strategy for “big boolean array” and “small boolean array” such that the user would just create a boolean array and the underlying implementation would be selected based on the requested size:

BoolArray<5> b1; // all values are initialized to false
// ^ the underlying internal data structure would be bool arr[5]

BoolArray<1005> b2; // all values are initialized to false
// ^ the underlying internal data structure is std::set<size_t>
//    holding only all the “true” indices (which are expected to be a few)

// following operations shall be supported:
// [1] simple assignment
b1[0] = true;
b2[0] = true;

// [2] toggle values using range-based-for
template<size_t SIZE>
void toggle_a_few(BoolArray<SIZE>& b) {
    for(auto&& val: b) {
        if(some_rare_case)
            val != val;
    }
}

You can find the solution for the exercise above here (don’t peek, try solving it by yourself first).

RValue and move semantics

In both surveys that we conducted, at CoreCpp 2021 and at CppCon 2021, rvalue and move semantics was considered a complex topic 10 years after being introduced into C++.

You should know what rvalue is and know the overload resolution of lvalue and rvalue.

As for move semantics and std::move, not using move semantics means waiving performance gains. Using it incorrectly means potential bugs.

Here are a few examples:

Forgetting to use std::move when needed:

class Pesron {
    std::string name;
public:
    Person(const std::string& p_name) : name(p_name) {}
    Person(std::string&& p_name) : name(p_name) {} // oops
    // ...
};

Can you tell what the problem is with the line marked with “oops”?

Another example of not using std::move when appropriate:

// popping the last element from a vector
auto val = a.back(); // allow call of move ctor if available
a.pop_back();

Can you tell how we can make the code above potentially more efficient?

Well, here it is:

auto val = std::move(a.back()); // allow call of move ctor if available
a.pop_back();

Other issues you should be aware of include the problem of implementing move forgetting `noexcept`, not using rule of zero when you can, losing good default move operations without getting them back with =default, using std::move when you shouldn’t (stealing from an object that you still use) and not using std::forward when relevant.

Placement new

The idea of placement new is that we can create an object at a specific given memory location by calling the constructor on that memory location without actually allocating memory. This is used by std::vector when a new item is added to an already allocated capacity. Knowing and understanding placement new is important for understanding the way std::vector is implemented. 

Strong types

With user defined literal types, it is now easy and straightforward to use strong types  like std::chrono does. The main idea is to keep your data tied to its measurement units. Wrong interpretation of measurement units is a known source of bugs crashing satellites (you may read more about the Ariane 5 crash here and here and about the Mars Climate Orbiter crash here).

If you do not use strong types yet, you may want to explore that further. See for example Joe Boccara’s strong types library and read about it here. Other strong type library implementations include:

Smart pointers

Again, based on the surveys we conducted, at CoreCpp 2021 and at CppCon 2021, memory leaks and debugging memory bugs are still an issue in 2022. This can be fixed by better use of C++ smart pointers.

Getting to know unique_ptr, shared_ptr and weak_ptr is important. Using smart pointers correctly would help you make your classes follow the rule of zero.

Implementing proper API with smart pointers is crucial for achieving the design goals of your system. Each smart pointer type has its own semantics and specific use case. Herb Sutter has a few classical posts on the subject that you may want to follow, see for example: GotW #89 Smart Pointers and GotW #91 Smart Pointer Parameters

Containers and algorithms

No need to introduce std::vector, but you may still want to make sure you avoid std::vector pitfalls. You should also know the C++17 new additions of std::optional, std::variant, std::any and std::string_view and when to use each. And the C++20 additions of std::span and the ranges library.

Knowing that std::array is an aggregate type and that it doesn’t value initializing its elements is also important (i.e. when creating an std::array of integers the values of the integers are not automatically initialized to zero by default, as opposed to std::vector which is zero initialized).

Question: How can you create an std::array of 100 integers and initialize them all to zero? (Note: std::array doesn’t have a proper constructor, in fact it doesn’t have any constructor.)

Answer:

std::array<int, 100> arr {}; // aggregate initialization, all ints are initialized to zero

And finally, knowing the secret behavior of vector of bools is of course something that even though is not essential to master, puts you in the experts camp.

As for algorithms, the important thing to know is to check for std algorithms (including ranges algorithms) before you implement your own. In many cases you would see that the algorithm you were about to implement is either existing or can be implemented using an existing algorithm.

Lambdas

Lambda expressions were added to C++ in C++11, and new and important syntax options were added in C++14, C++17 and C++20 (see this two parts blog post on the evolution of lambda: part 1 and part  2).

If you are using C++11 or above, you most probably know of lambda expressions. Make sure you are also aware of the new additions to it (see above links, as well as other resources like this playlist of Jason Turner’s C++ Weekly, on lambdas, if you watch them all you would really master lambdas!).

Constant evaluation

C++11 added the constexpr keyword, which allows the definition of constants that are assigned with a value known at compile time, and functions that can be potentially executed at compile time. Along the different versions of C++ the possibilities of constexpr evolved significantly, removing many of the restrictions that were on constexpr functions in C++11 (you can see the evolution in the constexpr cppreference page and in this very thorough blog post on the subject).

Knowing what you can do at compile time is useful for performance. Using C++17 if constexpr can allow you to write better generic code, avoiding unnecessary complicated overloads with SFINAE or concepts. Since C++20, std::string and std::vector have constexpr constructors and thus can be created at compile time.

C++20 also added consteval and constinit, which you may want to get acquainted with (e.g. with Jason Turner’s C++ Weekly videos on consteval and constinit).

Multithreading and concurrency

Multithreading and concurrency is a huge topic in general and in C++ specifically. Starting with the basic syntax of how to use std::thread would be a good starting point. Knowing about C++20 std::jthread would complete the basic knowledge.

Knowing how and where to use a mutex and lock guards is of course crucial when writing concurrent code. This includes the useful C++17 mutex wrapper for multiple mutexes, scoped_lock. It is good to add to the above knowledge the C++20 new locks, counting_semaphore and binary semaphore, as well as std::latch and std::barrier.

Understanding containers’ thread safety rules is important, as quite naturally at a certain point you would use the standard containers in your multithreaded application.

Then knowing how to use atomic variables and atomic operations such as compare_exchange can allow you to implement lock-free algorithms.

Getting to know std::promise, std::future and std::packaged_task would allow you to implement better multithreaded based async operations.

Finally, knowing how and when to use thread_local variables is also important.

The use of std::conditional_variable can be left for library implementers, but if you are in that position you should cover that as well.

As in other parts of this post, this is not a complete list of all the C++ concurrent classes and utilities (which can be found here). But it should pave the way to cover most of the important stuff.

New C++20 features

We already covered in a previous post the big four of C++20: Modules, Concepts, Coroutines and Ranges. We also mentioned in the above post the spaceship operator added to C++20. We also dedicated a post to coroutines. You should familiarize yourself with these new additions, but if you are not in C++20 yet, you are of course not required to master them.

Summary

We tried in this post to list C++ topics that we think advanced C++ developers should know and that are commonly in use. There are of course other advanced and important features and topics in C++ which were not covered. We skipped things that we see as basic. But we may also miss advanced features that could be added to the list. This is by no means an exhaustive list, it is almost certainly you can think of additional advanced C++ items that could have been added.

C++ is a very rich language, and it keeps evolving! Mastering it all is almost impossible. The goal of every C++ developer shall be to keep learning, keep an eye on new features (while making sure you don’t create gaps in your knowledge of old features) – to make sure you do not stay behind.

 

 

 

 

2022 best C++ ecosystems - Banner

Amir Kirsh
Amir Kirsh reading time: 11 minutes minutes April 4, 2022
April 4, 2022

Table of Contents

Related Posts

11 minutes 8 Reasons Why You Need Build Observability

Read More  

11 minutes These 4 advantages of caching are a game-changer for development projects

Read More  

11 minutes What Level of Build Observability Is Right for You?

Read More