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 |
|
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 |
|
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 |
|
This time Fun
is a function template with two template type parameters, T
and U
. The usual way to invoke this functions is:
1 |
|
However, since there is a default argument present for U
, we can use that:
1 |
|
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:
- Constraint placeholder types
- Abbreviated function templates with a template-head and constrained placeholder types
- 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 |
|
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 |
|
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 |
|
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 |
|
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 |
|
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 |
|
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.
Type | From right | From left |
---|---|---|
Default parameters of functions | X | |
Default arguments of template parameters | X | |
Constrained placeholder types | X | |
Abbreviated function templates with a template-head | X | |
Compound requirement | X |
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