Table of Contents

C++20 is huge and filled with lots of large features. Just to mention a few: Modules, Coroutines, Concepts, Ranges, Calendar & Timezone, Formatting library.

But, as you know, that’s not all.

Depending on how we count, C++20 brought around 80 Library features and 70 language changes, so there’s a lot to cover :)

In this article, I’ll show you 20 smaller C++20 things that are very handy and good to know. Ten language elements, and ten more for the Standard Library. Most of them with a cool example.

Let’s jump right into the text!

Documents and Sources  

You can find the whole C++20 draft here:

And here’s a great summary page with the compiler support at C++ Reference:

Here’s also another comparison of changes between C++17 and C++20:

Language Features  

Let’s start with improvements affecting the language.

1. Abbreviated Function Templates and Constrained Auto  

Thanks to the terse concept syntax, you can also write templates without the template<typename...> part.

With unconstrained auto:

void myTemplateFunc(auto param) { }

The code is equivalent to the following “regular” template style:

template <typename T>
void myTemplateFunc(T param) { }

Or with constrained auto - this time we specify a concept name that the type has to comply with:

template <class T>
concept SignedIntegral = std::is_signed_v<T> && std::is_integral_v<T>;

void signedIntsOnly(SignedIntegral auto val) { }

void floatsOnly(std::floating_point auto fp) { }

See at @Compiler Explorer.

And then it’s equal to:

template <class T>
concept SignedIntegral = std::is_signed_v<T> && std::is_integral_v<T>;

template <SignedIntegral T>
void signedIntsOnly(T val) { }

template <std::floating_point T>
void floatsOnly(T fp) { }

Additionally, template <SignedIntegral T> is also a short hand notation for:

template <typename T>
requires SignedIntegral<T>
void signedIntsOnly(T val) { }

template <typename T>
requires std::floating_point<T>
void floatsOnly(T fp) { }

See a simple demo @Compiler Explorer.

Such syntax is similar to what you could use in generic lambdas from C++14:

// C++14 lambda:
auto lambda = [](auto val) { };

// C++20 template function:
void myTemplateFunction(auto val) { }

See my separate blog post on Concepts: C++20 Concepts - a Quick Introduction - C++ Stories.

And more in the proposal: Yet another approach for constrained declarations - P1141R2.

2. Template Syntax For Generic Lambdas  

In C++14, we got generic lambdas with auto as a lambda parameter. However, sometimes it was not good enough. That’s why in C++20, you can also use “real” template argument syntax, also with concepts!

auto fn = []<typename T>(vector<T> const& vec) { 
    cout << size(vec) << ,  << vec.capacity(); 
};

Ream more in Lambda Week: Going Generic - C++ Stories and in the proposal: P0428r2.

3. Constexpr Improvements  

Lots of small features and improvements related to constexpr:

  • union - P1330
  • try and catch - P1002
  • dynamic_cast and typeid - P1327
  • constexpr allocation P0784
  • Virtual calls in constant expressions P1064
  • Miscellaneous constexpr library bits.

Thanks to those various bits, we have constexpr algorithms, and also std::vector and std::string can be used at compile time as well!

Here’s an example that shows several features that were unavailable before C++20:

#include <numeric>

constexpr int naiveSum(unsigned int n) {
    auto p = new int[n];
    std::iota(p, p+n, 1);
    auto tmp = std::accumulate(p, p+n, 0);
    delete[] p;
    return tmp;
}

constexpr int smartSum(unsigned int n) {
    return (1+n)*(n/2);
}

int main() {
    static_assert(naiveSum(10) == smartSum(10));
    return 0;
}

Play @Compiler Explorer.

See more about constexpr memory allocation in a separate blog post: constexpr Dynamic Memory Allocation, C++20 - C++ Stories

If you want to get more about C++20 constexpr in action, check out my article on The Balance Parentheses Interview Problem in C++20 constexpr - available for C++Stories Premium Members.

4. using enum  

It is a handy feature that allows you to control the visibility of enumerator names and thus make it simpler to write.

A canonical example is with switch:

#include <iostream>

enum class long_enum_name { hello, world, coding };

void func(long_enum_name len) {
#if defined(__cpp_using_enum) // c++20 feature testing
    switch (len) {
        using enum long_enum_name;
        case hello: std::cout << "hello "; break;
        case world: std::cout << "world "; break;
        case coding: std::cout << "coding "; break;
    }
#else
    switch (len) {
        case long_enum_name::hello: std::cout << "hello "; break;
        case long_enum_name::world: std::cout << "world "; break;
        case long_enum_name::coding: std::cout << "coding "; break;
    }
#endif
}

enum class another_long_name { hello, breaking, code };

int main() {
    using enum long_enum_name;
    func(hello);
    func(coding);
    func(world);
    
// using enum another_long_name; // error: 'another_long_name::hello' 
                             // conflicts with a previous declaration
}

Play with code @Compiler Explorer.

Thanks to using enum NAME, you can introduce the enumerator names into the current scope.

Read more in the proposal P1099 or at the Enumeration declaration - cppreference.com.

5. Class-types in non-type template parameters  

This feature is called NTTP in short.

Before C++20, for a non type template parameter, you could use:

  • lvalue reference type (to object or to function);
  • an integral type;
  • a pointer type (to object or to function);
  • a pointer to member type (to member object or to member function);
  • an enumeration type;

But since C++20, we can now add:

  • structures and simple classes - structural types
  • floating-point numbers
  • lambdas

A basic example:

#include <iostream>

template <double Ga>
double ComputeWeight(double mass) {
    return mass*Ga;
}

int main() {
    constexpr auto EarthGa = 9.81;
    constexpr auto MoonGa = 1.625;
    std::cout << ComputeWeight<EarthGa>(70.0) << '\n';
    std::cout << ComputeWeight<MoonGa>(70.0) << '\n';
}

Play @Compiler Explorer

And a bit more complex with a simple class:

#include <iostream>

struct Constants {
    double gravityAcceleration_ { 1.0 };

    constexpr double getGA() const { return gravityAcceleration_;}
};

template <Constants C>
double ComputeWeight(double mass) {
    return mass * C.getGA();
}

int main() {
    constexpr Constants EarthGa { 9.81 };
    constexpr Constants MoonGa { 1.625 };
    std::cout << ComputeWeight<EarthGa>(70.0) << '\n';
    std::cout << ComputeWeight<MoonGa>(70.0) << '\n';
}

Play @Compiler Explorer

In the example above, I used a simple class object, it’s “a structural type”. Here are the full options for such a type:

  • a scalar type, or
  • an lvalue reference type
  • a literal class type with the following properties:
    • all base classes and non-static data members are public and non-mutable and
    • the types of all bases classes and non-static data members are structural types or (possibly multi-dimensional) array thereof.

See all requirements in [temp.param 6]

Main benefits and use cases:

  • Make language more consistent. So far, C++ allowed simple types like enums, integral values,
  • Wrapper types
  • Compile-Time String Processing

Read more in the proposal P0732R2 and floating P1714 - floating-point, and the final wording and clarifications in P1907R1

6. Default bit-field initializers  

A tiny thing and can be treated as a “fix” for the feature introduced in C++11.

Since C++11 you can use non-static data member initialization and assign values directly inside the class declaration:

struct Type {
    int value = 100;
    int second {10001 };
};

As it appeared, the syntax failed and wasn’t working for bit-fields. C++20 improved this feature, and now you can write:

#include <iostream>

struct Type {
    int value : 4 = 1;
    int second : 4 { 2 };
};

int main() {
    Type t;
    std::cout << t.value << '\n';
    std::cout << t.second << '\n';
}

Play with code @Compiler Explorer.

Read more in the proposal: P0710r1

7. Designated Initializers  

A cool feature that we “stole” from C :)

In a basic form, you can write:

Type obj = { .designator = val, .designator { val2 }, ... };

For example:

struct Point { double x; double y; };
Point p { .x = 10.0, .y = 20.0 };

Designator points to a name of a non-static data member from our class, like .x or .y.

One of the main reasons to use this new kind of initialization is to increase readability.

Having the following type:

struct Date {
    int year;
    int month;
    int day;
};

This is easier to read:

Date inFuture { .year = 2050, .month = 4, .day = 10 };

Than:

Date inFuture { 2050, 4, 10 };

Here are the main rules of this feature:

  • Designated initializers work only for aggregate initialization, so they only support aggregate types.
  • Designators can only refer to non-static data members.
  • Designators in the initialization expression must have the same order of data members in a class declaration (in the C language, they can be in any order)
  • Not all data members must be specified in the expression.
  • You cannot mix regular initialization with designers.
  • There can only be one designator for a data member
  • You cannot nest designators.

Read more in the proposal: P0329r4

And I also have a separate article on this topic: Designated Initializers in C++20 - C++ Stories.

8. Nodiscard Attribute Improvements  

[[nodiscard]] - added in C++17, is a powerful attribute that might help to annotate important computation in code. In C++20 we get several improvements like:

  • [[nodiscard]] for constructors - P1771
  • [[nodiscard]] with a message P1301R4
  • Apply [[nodiscard]] to the standard library P0600R1

For example, with P1301, you can specify why the object shouldn’t be discarded. You might want to use this option to write about memory allocation or other important information that the compiler will report:

[[nodiscard("Don't call this heavy function if you don't need the result!")]] bool Compute();

What’s more thanks to P0600 this attribute is now applied in many places in the Standard Library, for example:

  • async()
  • allocate(), operator new
  • launder(), empty()

Read more in my separate blog post: Enforcing code contracts with nodiscard

9. Range-based for loop with Initializer  

A helpful way to enhance the syntax for range-based loops:

for (init; decl : expr)

For example:

#include <iostream>
#include <array>
#include <ranges>

void print(const std::ranges::range auto& container) {
    for (std::size_t i = 0; const auto& x : container) {
        std::cout << i << " -> " << x << '\n';
        // or std::cout << std::format("{} -> {}", i, x);
        ++i;
    }
}

int main() {
    std::array arr {5, 4, 3, 2, 1};
    print(arr);
}

Play with code @Compiler Explorer.

The initializer is also a good way to capture temporary objects:

for (auto& x : foo().items()) { /* .. */ } // undefined behavior if foo() returns by value
for (T thing = foo(); auto& x : thing.items()) { /* ... */ } // OK

See more in the proposal: P0614

This article started as a preview for Patrons, sometimes even months before the publication. If you want to get extra content, previews, free ebooks and access to our Discord server, join the C++ Stories Premium membership or see more information.

10. New keyword consteval - immediate functions  

The functionality is best described as the quote from the proposal:

The constexpr specifier applied to a function or member function indicates that a call to that function might be valid in a context requiring a constant-expression. It does not require that every such call be a constant-expression. Sometimes, however, we want to express that a function should always produce a constant when called (directly or indirectly), and a non-constant result should produce an error. Such a function is called an immediate function.

See example below:

consteval int sum(int a, int b) {
  return a + b;
}

constexpr int sum_c(int a, int b) {
    return a + b;
}

int main() {
    constexpr auto c = sum(100, 100);
    static_assert(c == 200);

    constexpr auto val = 10;
    static_assert(sum(val, val) == 2*val);

    int a = 10;
    int b = sum_c(a, 10); // fine with constexpr function

    // int d = sum(a, 10); // error! the value of 'a' is 
                           // not usable in a constant expression
}

See @Compiler Explorer.

Immediate functions can be seen as an alternative to function-style macros. They might not be visible in the debugger (inlined)

Additionally, while we can declare a constexpr variable, there’s no option to declare a consteval variable.

// consteval int some_important_constant = 42; // error

Declaring such variables required complicated definitions in the Standard for limited use cases, so this extension wasn’t added into the language.

Read more in the proposal P1073

constinit

There’s also another keyword that got into C++20 and starts with const. It’s constinit. While it’s a longer topic, I’d like to explain the main difference between those new keywords briefly,

In short, constinit allows us to declare a static storage duration variable that must be static initialized - i.e. zero initialization or constant initialization. This allows to avoid the static initialization order fiasco scenarios - see here: C++ FAQ.

See this basic example:

// init at compile time
constinit int global = 42;

int main() {
    // but allow to change later...
    global = 100;
}

Play @Compiler Explorer.

And see more c++ - What is constinit in C++20? - Stack Overflow.

The Standard Library  

Let’s now see some of the Standard Library changes.

11. Math Constants  

A new header <numbers> with a modern way to get most of common constants:

namespace std::numbers {
  template<class T> inline constexpr T e_v          = /* unspecified */;
  template<class T> inline constexpr T log2e_v      = /* unspecified */;
  template<class T> inline constexpr T log10e_v     = /* unspecified */;
  template<class T> inline constexpr T pi_v         = /* unspecified */;
  template<class T> inline constexpr T inv_pi_v     = /* unspecified */;
  template<class T> inline constexpr T inv_sqrtpi_v = /* unspecified */;
  template<class T> inline constexpr T ln2_v        = /* unspecified */;
  template<class T> inline constexpr T ln10_v       = /* unspecified */;
  template<class T> inline constexpr T sqrt2_v      = /* unspecified */;
  template<class T> inline constexpr T sqrt3_v      = /* unspecified */;
  template<class T> inline constexpr T inv_sqrt3_v  = /* unspecified */;
  template<class T> inline constexpr T egamma_v     = /* unspecified */;
  template<class T> inline constexpr T phi_v        = /* unspecified */;
}

Those numbers are variable templates, but there are also helper inline constexpr variables like:

inline constexpr double pi = pi_v<double>;

Simple demo:

#include <numbers>
#include <iostream>

int main() {
    std::cout << std::numbers::pi << '\n';
    using namespace std::numbers;
    std::cout << pi_v<float> << '\n';
}

Play @Compiler Explorer.

Read more in P0631 and at Cppreference.

12. More constexpr in the Library  

C++20 improved the language rules for constexpr but then the Standard Library also took those features and added them to library types. For example:

  • constexpr std::complex
  • constexpr algorithms P0202
  • Making std::vector constexpr - P1004
  • Making std::string constexpr - P0980

With all of the support, we can write the following code that calculates the number of words in a string literal, all at compile time:

#include <vector>
#include <string>
#include <algorithm>

constexpr std::vector<std::string> 
split(std::string_view strv, std::string_view delims = " ") {
    std::vector<std::string> output;
    size_t first = 0;

    while (first < strv.size()) {
        const auto second = strv.find_first_of(delims, first);

        if (first != second)
            output.emplace_back(strv.substr(first, second-first));

        if (second == std::string_view::npos)
            break;

        first = second + 1;
    }

    return output;
}

constexpr size_t numWords(std::string_view str) {
    const auto words = split(str);

    return words.size();
}

int main() {
    static_assert(numWords("hello world abc xyz") == 4);
}

See @Compiler Explorer.

And also, you can have a look at another article: constexpr vector and string in C++20 and One Big Limitation - C++ Stories.

Would you like to see more?
I wrote a constexpr string parser and it's available for C++ Stories Premium/Patreon members. See all Premium benefits here.

13. .starts_with() and .ends_with()  

Finally, a handy way to check prefixes and suffixes for strings in C++!

Let’s see an example:

#include <string>
#include <iostream>
#include <string_view>

int main(){
    const std::string url = "https://isocpp.org";
    
    // string literals
    if (url.starts_with("https") && url.ends_with(".org"))
        std::cout << "you're using the correct site!\n";
    
    if (url.starts_with('h') && url.ends_with('g'))
        std::cout << "letters matched!\n";
}

Play @Wandbox.

I wrote a separate blog post on this topic with more examples, so have a look: How to Check String or String View Prefixes and Suffixes in C++20 - C++ Stories.

Some other use cases (also suggested by your comments at r/programming):

  • finding files with a certain ending (checking file name or extension)
  • finding files with a specific beginning
  • finding lines in a text file starting with some date or prefix
  • parsing custom text file formats

And here’s the link to the proposal P0457.

14. contains() member function of associative containers  

When you want to check if there’s an element inside a container, you can usually write the following condition:

if (container.find(key) != container.end())

For example (based on Cppreference):

std::map<std::string, int, std::less<>> strToInt = {
        {"hello", 10},
        {"world", 100}
    };
 
for(auto& key: {"hello", "something"}) {
    if(strToInt.find(key) != strToInt.end())
        std::cout << key << ": Found\n";
    else
        std::cout << key << ": Not found\n";        
}

We can now rewrite into:

for(auto& key: {"hello", "something"}) {
    if(strToInt.contains(key))
        std::cout << key << ": Found\n";
    else
        std::cout << key << ": Not found\n";
}

Play with code @Compiler Explorer

As you can see, the code is more readable as the one function explains what the code does.

What’s important is that the check can also be “transient” and “heterogeneous” that’s why I declared the container as std::map<std::string, int, std::less<>>.

See the proposal: P0458R2

Note: in C++23 we already have similar functions for strings! See string.contains @Cppreference.

You can also read more about handy map functions in a separate article @C++ Stories: Examples of 7 Handy Functions for Associative Containers in Modern C++.

15. Consistent Container Erasure  

A handy wrapper for the remove/erase idiom for many containers in the Standard Library!

The std::remove algorithm does not remove elements from a given container as it works on the input iterators. std::remove only shifts elements around so that we can call .erase() later. Such a technique appeared to be error-prone, hard to learn and teach.

In C++20, we get a bunch of free functions that have overloads for many containers and can remove elements:

erase(container, value);
erase_if(container, predicate);

See the example:

#include <iostream>
#include <vector>

int main() {
    std::vector vec { 1, 2, 3, 4, 5, 6, 7, 8, 9, 10 };
    std::erase_if(vec, [](auto& v) { return v % 2 == 0; });
    for (int i = 0; auto &v : vec) 
        std::cout << i++ << ": " << v << '\n';
}

If we go through the proposal, we can see all the changes, for example:

void erase(basic_string<charT, traits, Allocator>& c, const U& value);

Is equivalent to: c.erase(remove(c.begin(), c.end(), value), c.end());

But for associative containers:

void erase_if(map<Key, T, Compare, Allocator>& c, Predicate pred);

Is equivalent to:

for (auto i = c.begin(), last = c.end(); i != last; ) {
    if (pred(*i))
        i = c.erase(i);
    else
        ++i;
}

See more in the proposal P1209

16. Source Location  

A modern way to capture current file, function, or line information about source code.

So far the common tecnique was to use special macros:

void MyTrace(int line, const char *fileName, const char *msg, ...) { }
#define MY_TRACE(msg, ...) MyTrace(__LINE__, __FILE__, msg, __VA_ARGS__)
MYTRACE("Hello World");

But now we have a special helper type std::source_location that is a regular C++ object and can be passed in functions:

template <typename ...Args>
void TraceLoc(const source_location& location, Args&& ...args) {
    std::ostringstream stream;
    stream << location.file_name() 
           << "(" << location.line()
           << ", function " << location.function_name() << "): ";
    (stream << ... << std::forward<Args>(args)) << '\n';

    std::cout << stream.str();
}
 
int main() {
    TraceLoc(source_location::current(), "hello world ", 10, ", ", 42);
}

The above code might generate:

main.cpp(22, function main): hello world 10, 42

Have a look at the code at Wandbox

Read more in the proposal: P1208.

And in my separate article: Improving Print Logging with Line Pos Info & Modern C++ - C++ Stories.

17. std::bind_front - for partial function application  

P0356R5 and P1651R0

It’s an enhancement for std::bind for partial function application. It’s easier to use and it has more compact syntax:

using namespace std::placeholders;
auto f1 = std::bind(func, 42, 128, _1,_2);
// vs
auto f2 = std::bind_front(func, 42, 128);
        
f1(100, 200);
f2(100, 200);

Play with the example @Compiler Explorer.

It perfectly forwards the arguments into the callable object, but contrary to std::bind doesn’t allow to reorder arguments.

In addition, bind_front is more readable and easier to write than a corresponding lambda function object. To achieve the same result, your lambda would have to support perfect forwarding, exception specification, and other boilerplate code.

18. Heterogeneous lookup for unordered containers  

In C++14, we got a way to search for a key in an ordered container by types that are “comparable” to the key. This enabled searching via const char* in a map of std::string and added potential speed improvements in some cases.

C++20 fills the gap and adds the support for unordered containers like unordered_map or unorderd_set and others.

See the example:

struct string_hash {
  using is_transparent = void;
  [[nodiscard]] size_t operator()(const char *txt) const {
    return std::hash<std::string_view>{}(txt);
  }
  [[nodiscard]] size_t operator()(std::string_view txt) const {
    return std::hash<std::string_view>{}(txt);
  }
  [[nodiscard]] size_t operator()(const std::string &txt) const {
    return std::hash<std::string>{}(txt);
  }
};

int main() {
  auto addIntoMap = [](auto &mp) {
    mp.emplace(std::make_pair("Hello Super Long String", 1));
    mp.emplace(std::make_pair("Another Longish String", 2));
    mp.emplace(std::make_pair("This cannot fall into SSO buffer", 3));
  };

  std::cout << "intMapNormal creation...\n";
  std::unordered_map<std::string, int> intMapNormal;
  addIntoMap(intMapNormal);

  std::cout << "Lookup in intMapNormal: \n";
  bool found = intMapNormal.contains("Hello Super Long String");
  std::cout << "Found: " << std::boolalpha << found << '\n';

  std::cout << "intMapTransparent creation...\n";
  std::unordered_map<std::string, int, string_hash, std::equal_to<>>
      intMapTransparent;
  addIntoMap(intMapTransparent);

  std::cout << "Lookup in map by const char*: \n";
  // this one won't create temp std::string object!
  found = intMapTransparent.contains("Hello Super Long String");
  std::cout << "Found: " << std::boolalpha << found << '\n';

  std::cout << "Lookup in map by string_view: \n";
  std::string_view sv("Another Longish String");
  // this one won't create temp std::string object!
  found = intMapTransparent.contains(sv);
  std::cout << "Found: " << std::boolalpha << found << '\n';
}

Play with code @Compiler Explorer

For ordered containers, we only need a “transparent” comparator function object. In the case of unordered containers, we also need a hasher to support compatible types.

The initial proposal P0919R3 and final updates: P1690R1.

See my separate article on this feature (and also from C++14): C++20: Heterogeneous Lookup in (Un)ordered Containers - C++ Stories.

19. Smart pointer creation with default initialization  

When you allocate an array, you can write the following code:

new T[]()
// vs
new T[]
  • The first is “value initialization,” and for arrays, initializes each element to zero (for built-in types) or call their default ctors.
  • The latter is called default initialization and, for built-in types, generates indeterminate values or calls default ctor.

For buffers, it’s pretty common not to clear the memory as you might want to overwrite it immediately with some other data (for example, loaded from a file or network).

As it appears when you wrap such array allocation inside a smart pointer, then the current implementations of make_unique and make_shared used the first form of the initialization. And thus, you could see a small performance overhead.

With C++20, we got an option to be flexible about that initialization and still safely use make_shared/make_unique.

Those new functions are called:

std::make_unique_for_overwrite
std::make_shared_for_overwrite
std::allocate_shared_for_overwrite

In C++20 you can write:

auto ptr = std::make_unique_for_overwrite<int[]>(COUNT);

Would you like to see more?
To see benchmarks have a look at this premium blog post for Patrons: "Smart Pointers Initialization Speedup in C++20 - Benchmarks" and it's available for C++ Stories Premium/Patreon members. See all Premium benefits here.

See the reasoning and the initial proposal in P1020R1.

Side note: this feature was voted in as make_unique_default_init, but the naming was changed into _for_overwrite in the paper: P1973R1.

And have a look at my separate article on: C++ Smart Pointers and Arrays - C++ Stories.

20. Safe integral comparisons  

When you compare:

const long longVal = -100;
const size_t sizeVal = 100;
std::cout << std::boolalpha << (longVal < sizeVal);

This prints false as longVal is converted to size_t and now has the value of std::numeric_limits<size_t>::max()-100+1. See here @Compiler Explorer.

Sometimes such unsigned to signed comparisons are handy and that’s why in C++20 In the Standard Library we’ll have the following new functions in the <utility> header:

template <class T, class U>
constexpr bool cmp_equal (T t , U u) noexcept
template <class T, class U>
constexpr bool cmp_not_equal (T t , U u) noexcept
template <class T, class U>
constexpr bool cmp_less (T t , U u) noexcept
template <class T, class U>
constexpr bool cmp_greater (T t , U u) noexcept
template <class T, class U>
constexpr bool cmp_less_equal (T t , U u) noexcept
template <class T, class U>
constexpr bool cmp_greater_equal (T t , U u) noexcept
template <class R, class T>
constexpr bool in_range (T t) noexcept

T and U are required to be standard integer types: (un)signed char, int, short, long, long long, uint8_t.... Those functions cannot be used to compare std::byte, char8_t, char16_t, char32_t, wchar_t and bool.

With those functions, you can compare values of different types with the “mathematical” meaning.

For example:

We can rewrite our example into

const long longVal = -100;
const size_t sizeVal = 100;
std::cout << std::boolalpha;
std::cout << std::cmp_less(longVal, sizeVal); 

See the code at @Compiler Explorer.

And now the code prints true.

See more in the proposal P0586

Bonus - other cool features  

As I mentioned in the introduction, in C++20, we have around 70 language features and 80 library changes. Below you can find a table with brief notes on other cool elements.

The language features first:

Feature Notes
Attributes [[likely]] and [[unlikely]] See my bonus article at Patreon (Free) - C++ attributes, from C++11 to C++20
Make typename more optional See my separate blog post: Simplify template code with fewer typename in C++20 - C++ Stories
Attribute [[no_unique_address]] See my article: Empty Base Class Optimisation, no_unique_address and unique_ptr - C++ Stories
explicit(bool) The explicit keyword can be applied conditionally, useful for wrapper template types.
Feature test macros Standardized macros that describe if a given feature is available in your compiler. See Improve Multiplatform Code With __has_include and Feature Test Macros - C++ Stories
Parenthesized initialization of aggregates Improves consistency in template code! You can now write int ab[] (1, 2, 3);

And also more library parts:

Feature Notes
std::basic_osyncstream Synchronized buffered output
std::to_address Get the address represented by p in all cases
std::lerp() and std::midpoint() More numeric functions!
std::to_array Allows shorter notation and type/size deduction
Bit manipulation function bit_cast, byteswap, bit_ceil, bit_width, popcount and more bit functions!

Summary  

Throughout this blog post, I hope you’ve found some features that might be immediately applied to your code. From more minor language things like bit fields and NSDMI to using enum or initializer for range-based for loop. And then library features like math constants, starts_with or heterogeneous lookup. Most of the areas for C++ are covered.

If you want to check all features from C++20 supported by your compiler, visit this handy page at cppreference: C++20 compiler support.

See the similar C++17 article: 17 Smaller but Handy C++17 Features - C++ Stories.

Back to you

  • What’s your favorite smaller feature from C++20?
  • Have you used C++20 in production?

Join the discussion below in the comments or at the following /reddit/r/cpp thread.