Logo

Blog


C++20 - Filling blanks

What do you know about default parameters in C++? Well, C++20 introduced new elements that can be seen like default parameters.

Already known: Default parameters of functions

That in C++ functions can have default parameters is probably no big news.

1
void Fun(int x, int x, int z = 0);

In the example above, the function Fun takes three parameters. One of them z is defaulted to 0. This allows us to call Fun with either two or three parameters:

1
2
Fun(2, 3); A 
Fun(2, 3, 4);

In the case of A, the compiler injects the 0 such that the call effectively looks like Fun(2, 3, 0).

Already known: Default arguments of template parameters

Another instance of default parameters are defaulted template arguments:

1
2
template<typename T , typename U = int>
void Fun() {}

This time Fun is a function template with two template type parameters, T and U. The usual way to invoke this functions is:

1
Fun<char, int>();

However, since there is a default argument present for U, we can use that:

1
Fun<char>();

The call to Fun results in the same call as before when we explicitly specified int. Feel free to use C++ Insights to verify this.

New elements of C++20

All right, we look at the past now, let's see the additions of C++20. We are looking at three new places which I will walk you through:

  1. Constraint placeholder types
  2. Abbreviated function templates with a template-head and constrained placeholder types
  3. Compound requirement

In all these cases, we can have a scenario where an argument can be defaulted.

Constraint placeholder types

In C++20, we have Concepts that allow us to constrain placeholder types. The auto in an abbreviated function template is such a placeholder type.

Abbreviated function templates are a new element of C++20. They allow us to use auto as a function parameter:

1
void Fun(auto x);

The definition of Fun is essentially a function template. The compiler does the transformation for us, leaving us with a nice short syntax. You may already know this from C++14's generic lambdas.

For the following, assume that we have two classes, A and B, where B derives from A. Further, we like to have a function template Fun which takes a single auto parameter. This parameter is constrained with std::derived_from to ensure that Fun is only called with types that have A as a base class. Because Fun takes the parameter by value, we cannot use the base class. This could result in slicing. Our code then looks like this:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
#include <concepts>
struct A {};

struct B : A {};

void Fun(std::derived_from<A> auto x);

int main() {
  Fun(B{});
}

The part where default parameters come into play is the constraint std::derived_from for the placeholder type. Looking closely at the code, you can see that derived_from is called only with one parameter, A. Yet the definition of derived_from requires two parameters. How else could derived_from do its check? However, the code as presented works fine. The reason for that is that the compiler has the power to inject parameters into concepts. Internally the compiler injects B, the type auto deduces, as the first argument to derived_from:

1
void Fun(std::derived_from<B, A> auto x);

Aside from the fact that this is very neat, we are looking at something new. This is the first time default parameters, or better omitted parameters, get inserted from the left. In the previous cases, the compiler starts filling from the right.

Abbreviated function templates with a template-head and constrained placeholder types

One variation of the above is once we mix abbreviated function templates with a template-head:

1
2
3
4
5
6
template<typename T>
void Fun(std::derived_from<A> auto x);

int main() {
  Fun<int>(B{});
}

In this specific case, the compiler appends a template parameter to the template-head for our auto-parameter, yet std::derived_from is still filled from the left.

Wrap Fun in a namespace to see how it is treated internally with C++ Insights.

One interesting thing we can do with that is having a variadic template parameter followed by another template parameter:

1
2
3
4
5
6
template<typename... Ts>
void Fun(std::derived_from<A> auto x);

int main() {
  Fun<int, char>(B{});
}

We cannot have this without auto-parameters. However, this is the only form I know of that works. As soon as you try using the parameter pack as function arguments, it stops working. The compiler doesn't know when the pack is terminated.

A compound requirement

With Concepts, we got a requires expression that can host a compound requirement. The purpose of a compound requirement is to check:

  • If a function is noexcept
  • Whether the return type of a function satisfies a concept.

We can check only one of them or both. For the following example, only the second check is used:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
template<typename T>
concept Silly = requires(T t)
{
  { t.Fun() } -> std::derived_from<A>;
};

struct C {
  B Fun();
};

static_assert(Silly<C>);

With this piece of code, we ensure with the help of the concept Silly, that the member function Fun of a class T returns a type that is derived from A. In the derived_from check, we see the same pattern we previously saw in constraint placeholder types. The compiler injects the missing argument, once again from the left. This is important because the check would not work if the compiler filled the right value.

In a nutshell

The table provides an overview of the various elements in C++ where the compiler fills in the blanks for use when it comes to parameters.

TypeFrom rightFrom left
Default parameters of functionsX
Default arguments of template parametersX
Constrained placeholder typesX
Abbreviated function templates with a template-headX
Compound requirementX

Diving into C++20

In case you like to learn more about C++20's Concepts, consider my book Programming with C++20.

In 2021 I gave various talks about Concepts. Here is one recording from CppCon: C++20 Templates: The next level: Concepts and more.

Andreas