C++20

The final meeting for new features in C++ is over, so let’s explore the new features in C++, from a data science point of view. This is the largest release of C++ since C++11, and when you consider C++14 and C++17 to be interim releases, the entire 9 year cycle is possibly the largest yet! It may not feel quite as massive as C++11, since we didn’t have interim releases for C++11 and because C++11 is a much more complete, useful language than C++03, but this is still a really impactful release! This is also the first version to include every major feature from Bjarne Stroustrup’s “The design and Evolution of C++”, also known as D&E, from 1994!

Let’s look at the major new features, as well as collections of smaller ones.


Posts in the C++ series11 14 17 20 23

Modules

Since the beginning, C++ (and C) have remained just about the only language without some sort of module system. The current method for organizing code is simply a hack called #include. The compiler just inserts the contents of the included file. Finally, there is a built in method to organize your code!

Here is what it would look like. At the top of a library file, you would write:

export module mylib;

Then, on any item (function, class, etc) that you want to make available to users, you would write:

export int myfunct() {return 42};

To use this, you would import it:

import mylib;
...
myfunction();

This feature is orthogonal to namespaces; you should be using both. Unlike normal includes, other stuff (like macros, other includes, etc.) is not leaked unless you explicitly export it. You can break up “header” and implementation if you want to by putting defined in the module file, then prefixing the implementation file with:

module mylib;

Future:

There seem to be plans to do things like provide a standard method to declare structure in a future revision; while this won’t be a full standard build system, it might really help.

Constexpr all the things!

This is a broad collection of work heavily inspired by the famous talk, Constexpr all the things!. The short version of this is that constexpr is being added to lots of places. For functionality, we have constexpr new, try/catch, union, dynamic_cast, allocation, virtual function calls, and typeid. We also have consteval, which must be used in a constexpr context, and constinit, which requires that a static variable have an initializer that is constexpr. Inline assymbily is now allowed in a constexpr function as long as it is not evaluated.

All these together allow some great new features in the library, such as constexpr std::vector and std::string! We also have std::is_constant_evaluated, which while not quite as simple as it sounds1, does allow you to provide constexpr and non-constexpr code in a single function. The swap related algorithms now are constexpr too, which include things like sorting.

This will be key in allowing reflection, compile time programming, and metaclasses. You should be able to do things like create a parser for json that can turn on and off code at compile time based on a json formatted string in your source-code (the Constexpr all the things! goal, but with fewer workarounds).

Concepts

This is transformational for template programming, making it clearer, safer, and much easier to write and use code that is only valid for some arguments (which is pretty much all templated code; if you do anything at all with the item you passed in, you are making assumptions about it). It doesn’t really add functionality; it just helps you forget the arcane manipulations you used to have to do to (miss-)use SFINAE for this. Here’s an example of use first:

// Concept definition
template<class T>
concept integral = std::is_integral<T>::value;

// Standard short syntax
template<integral T>
std::string f_integral(T) {
    return "Yes";
}

// Very shorthand syntax
// std::string f_integral(integral auto) { ...

// Full syntax
template<class T>
requires (!integral<T>)
std::string f_integral(T) {
    return "No";
}

Try it in GCC 12 here. There used to be multiple ways to do this, all requiring hacky magic workarounds, like adding a spare template argument that is never supposed to be given and void types.

Classic example (click here)

This is a simple example; there are other cases that require different solutions, like classes. I’m simply using std::is_integral directly; you often will be building your own with more requirements and decays, etc.

template<class T, typename std::enable_if<std::is_integral<T>::value, T>::type* = nullptr>
std::string f_integral(T) {
    return "Yes";
}

template<class T, typename std::enable_if<!std::is_integral<T>::value, T>::type* = nullptr>
std::string f_integral(T) {
    return "No";
}

You can also use concepts for variable definitions:

integral auto x = my_function();

You get nice error messages from the compiler if you fail to fulfill the requirements; the error message now can tell you the correct problem at the correct location in your code!

Ranges

This feature was waiting on Concepts. It drops the old iterator methods (begin and end) with a “view” concept. Because you no longer repeat yourself (DRY principle), code is shorter and clearer. And you can chain these operations, dropping unneeded explicit memory usage, giving the compiler the option to optimize. This also gives you a new way to think about problems.

Simple example:

std::vector<int> values{1,3,2};
std::ranges::sort(values);

You can build up operations functionally and then chain them together. For example, let’s say you want to sum the squares of a vector:

std::vector<int> values{1,3,2};
std::string sum_of_squares = std::ranges::accumulate(
    values | std::ranges::transform([](int&& i){return i*i;}),
    0 // starting value
);

See the primary author’s fantastic post here. Note we also get std::span in C++20.

Coroutines

This feature allows resumable functions in C++, very much like a combination of Python’s yield and async generators. A very simple example (borrowing std::generator from C++23 to keep the example simple):

std::generator<int> make_three_ints() {
    co_yield 1;
    co_yeild 2;
    co_yeild 3;
}
...
    for (auto i : make_three_ints()) {
        std::cout << i << std::endl;
    }

See an example here. There’s a lot more, like awaiting, but that gives you a little taste. There is an interesting proposal to use async definitions and a simpler syntax, but it probably won’t make it in to C++20 unless they delay the standard. Also, be very careful about dangeling references, which are really easy to make with coroutines if you follow best practices for functions.

Formatting

If you have ever tried to do anything with text, you will probably realize that your choices are pretty poor in C++. The classic C-style printf is not really C++ and not ideal for strings, does not support custom types, and has other drawbacks. The C++ solution, operator<< and streams, is verbose and very unwieldy, and still very poor for building strings. C++11 and later provide variadic templates and the necessary type tools to build a better solution. The common but poor % style formatting is also not as elegant as Python’s format solution, which is being picked up by other languages (such as rust).

There was a fantastic library for C++11 called fmtlib that built this Python inspired format language into a varaitic C++ solution that was incredibly fast and extensible. That has now landed as std::format! Example:

std::string output = std::format("This is C++{}", 20);

You can easily control the values, refer to arguments by position, and even nest arguments in the format specifiers! This lets you easily build advanced formatting into a single string. And you can extend this to your own types. Unfortunately, this only includes fmt::format and not fmt::print, but it is a great start.

Usability features

While compilers and authors have already been using them, feature test macros are now an official part of C++20, so C++20 and beyond will include feature test macros for features; combined with __has_include from C++17, this enables the code to adapt itself to the features available, rather than having to depend on a build system and lots of custom definitions.

The space ship operator, <=>, has been added and is being applied throughout the standard library. This simplifies the number of overloads a class author has to define to fully cover all possible relationships (==, !=, <, >, >=, and <=), and can be useful as an operator as well.2

I am sure you have tried to use a using declaration on a scoped enum at some point; well, now it works. You can now write:

enum class color { red, green, blue };
...
using enum color;
if(x == red) ...

You can finally retire the old unscoped enums!

Other features

Range range-based for loops also now can take an initializer statement too, just like normal for has always had and if/while gained in C++17.

We finally have math constants! That means pi is finally in the standard library! They use templated variables to provide specializations for different types, and you can add your own specializations.

#include <numbers>
std::cout << "Pi is " << std::numbers::pi<double>;

You can also calculate midpoints between numbers with std::midpoint, as well as do linear interpolation with std::lerp; both take more care for accuracy and overflow than the average programmer would when computing these simple functions.

Another nice feature is source_location, a standard struct for the source location instead of platform specific tricks. You also have std::stop_token and automatically joining threads (jthread).3

There are new standalone standard bit operations. std::execution::unsequenced_policy is now in the standard; it’s just a logical extension of the possible multithreading requests, and a potentially very useful one in threaded environments.

Standard library containers finally get a .contains method, similar to the in keyword in some other languages.

Minor other features: __VA_OPT__ lets you finally handle commas correctly in preprocessor macros, typename can now be omitted in more contexts, lambdas can use traditional template syntax, functions can use the auto syntax, and we now have designated initializers (which brings us a bit closer to named parameters). You can also drop the template syntax in more places with more deduction guides in the standard library. Classes can now be used as non-type template parameters (instead of just int and char), though there may be some issues.

Reminder on C++14 and C++17

While I still view C++14 as a “bug-fix” release for C++11, it does have some very small but very nice additions. The new things in C++11 tend to be more usable and well rounded in C++14. Templated literals, more constexpr, more type tools, standard library literals, and auto in more places, including the powerful auto lambda, are the main highlights.

C++17 is a bigger step forward, and feels like a “preview” of C++20. You have structured bindings, which allow multiple return values to become a language supported feature. You can put an initializer statement into if and switch, like you can already do for for. Copy elision allows you to avoid a copy in some special cases. Template argument deduction finally allows templated classes to be deduced just like functions are, finally eliminating the need for many of the make_* functions.

Smaller additions include nested namespaces, fold expressions, inline variables, and __has_include.

There were three large library additions; the parallel standard library provides built-in support for parallel execution (and is rather slowly becoming available). The filesystem library was also slow to be adopted by the standard library implementations, but is mostly available and greatly simplifies cross-platform path manipulations. The third library addition is a set of new helper types: std::optional, std::variant, and std::any, which are amazing. Just to recap these:

  • std::optional<T> is either of type T or false. This removes the need to use a pointer to indicate if something can be “null”.
  • std::variant<A,B,C> can be either A, B, or C, and provides C++ techniques for working with all the possible values. The C feature this replaces is called a union.
  • std::any erases the type of T to store it (this is the only one that uses the heap), but is still safer than a void *.

Together, these features remove many of the remaining reasons to use pointers to do things pointers should not be used for.




Further reading


Posts in the C++ series11 14 17 20 23


  1. There are several places where a function will not be constexpr where you might expect it to be; read the docs to learn how best to use constexpr unambiguously. ↩︎

  2. You can still define ==; this can be helpful in some cases. ↩︎

  3. I’m assuming that writing custom threaded code is not something the average scientist has to do often, so I’m not making this a bigger point. But if you do, it’s really fantastic. Along with some other great threading improvements. ↩︎

comments powered by Disqus