C++20 Concepts: Testing constrained functions
In my last month post How C++20 Concepts can simplify your code I introduced the different kind of a requires-clause, part of C++20s Concepts. Concepts and the requires-clause allow us to put constraints on functions or classes and other template constructs. In today's post I like to build and example applying constraints to a function template and then look how to test them. Having constraints is a nice thing, but are they worth if they go untested?
The difference between a requires-clause and a requires-expression
In the last post I only showed a requires-clause and the three valid places such a clause can be, as a requires-clause, a trailing requires-clause, and when creating a concept. But there is another requires-thing, the requires-expression. And guess what, there is more than one kind of requires-expression. But hey, you are reading a post about C++; you had it coming.
A requires-expression has a body which itself has one of multiple requirements. The expression can have an optional parameter list. Those a requires-expression looks like a function called
requires except for the return-type which is implicitly
Now, inside of a requires-expression we can have four distinct types of requirements:
- Simple requirement
- Nested requirement
- Compound requirement
- Type requirement
This kind asserts the validity of an expression. For example,
a + b is an expression. It requires that there is an
operator+ for these two types. If there is one, it fulfils this requirement, otherwise we get a compilation error.
A nested requirement asserts that an expression evaluates to
true. A nested requirement always starts with
requires. So we have a
requires inside a requires-expression. And we don't stop there, but later more. With a nested requirement we can apply a type-trait to the parameters of the requires-expression. Beware that this requires a boolean value, so either use the
_v version of the type-trait or
::value. Of course, this is not limited to type-traits. You can supply any expression which evaluates to
With a compound requirement we can check the return type of an expression and optional if the expressions result is
noexcept. As the name indicates, a compound requirement has the expression in curly braces, followed by the optional
noexcept and something like a trailing return-type. Just that this trialing part needs to be a concept against which we check the result of the expression.
The last type of requirement we can have inside a requires-expression is the type requirement. It looks much like a simple requirement, just that it is introduced by
typename. It asserts that a certain type is valid. We can use it to check whether a given type has a certain subtype, or if a class template is instantiable with a given type.
An example: A constrained variadic function template
Let's let code speak. Assume that we have a variadic function template
1 2 3 4 5
It used fold expression to execute the plus operation to all values in the parameter pack
args and provides an initial value with
arg. We are looking at a binary left fold. This is a very short function template. However, the requirements to a type are hidden. What is
typename? Any type, right? But wait, it must at least provide
operator+. The parameter pack can take values of different types, but what if we like to constrain it to all types be of the same type? And do we really want to allow a throwing
operator+? Further as
auto, what if
operator+ of a type returns a different type? Do we really want to allow that? Oh yes, and there is the question whether
add makes sense with just a single parameter which leads to an empty pack. Doesn't make much sense to me to add nothing. Let's bake all that in requirements. We like
- The type must provide
- Only same types passed to
- At least two parameter are required, such that the pack is not empty.
- and return an object of the same type
Before we start with the requires-expression we need some additional type-traits. The function template signature has only a parameter pack. For some of the tests we need one type out of that pack. Therefore, a type-trait
first_type_t helps us to split the first type from the pack. For the check whether all types are of the same type, we define a variable template
std::conjunction_v to apply
std::is_same to all elements. Third, we need a concept
same_as_first_type to assert the return type with a compound requirement. It used
first_type_t to compare the return type of the compound requirement to the first type of the parameter pack. Here is a sample implementation1.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20
As you can see, we expect that the compiler inserts the missing template parameter for
same_as_first_type as the first parameter. In fact, the compiler always fills them from the left to the right in case of concepts.
Now that we have the tools let's create the requires-expression.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16
The numbers of the callouts in the example match the requirements we listed earlier. That would be the first part. We now have a constraint function template using three out of four possible requirement kinds. You have probably accustomed to the new syntax, so does
clang-format, but I hope you can see that we not only have constrained
add we also added documentation to it. It is surprising how many requirements we had to a type for just a one-line function-template. Now think about your real-world code and how hard it is there sometimes to understand why a certain type causes a template instantiation to error.
Testing the constraints
Great, now that we have this super constrained and documented
add function, why would you believe me that all the requirements are correct? No worries, I expect you to not trust me so far, I wouldn't trust myself.
What strategy can we use to verify the constraints? Sure, we can create small code snippets which violate one of the assertions and ensure that the compilation fails. But come on, that is not great and cumbersome to repeat. We can do better!
Whatever the solution is, so far we can say that we need a mock object which can have a conditional
operator+ and that operator can be conditionally disabled. I like my code to be unique, without copy and past parts, hence a class template sound good. In the last part we have seen how we can conditionally disable a method using a NTTP and
requires. Passing the
noexcept status as another NTTP is simple. A mock class can look like this:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27
1 we create a class template called
ObjectMock taking two NTTP of type
bool. It has an
operator+ 2 which has the conditional
noexcept controlled by
NOEXCEPT the first template parameter and a matching return-type. The same operator is controlled by a trailing requires-clause which disables it based on
hasOperatorPlus, the second template parameter. The second version 3 is the same, except that is returns a different type and with that does not match the expectation of the requires-expression of
add. A third NTTP,
validReturnType, controls two different operators 2 and 3, it enables only one of them.
In 4 we define three different mocks with the different properties. With that we have our mock.
A concept to test constraints
The interesting question is now, how do we test the
add function? We clearly need to call it with the different mocks and validate that is fails or succeeds but without causing a compilation error. The answer is, we use a combination of a concept wrapped in a
static_assert. Let's call that concept
TestAdd. We need to pass at least either one or two types to it, based on our requirement, that
add should not work with just one parameter. That calls for a variadic template parameter of the concept. Inside the requires-expression of
TestAdd we make the call to
add. There is one minor thing, we need values in order to call
add. Do you remember, a requires-expression can have a parameter list. We can use the parameter pack and supply it as a parameter list. After that we can expand the pack when calling
1 2 3 4 5 6
Wrap the test concept in a
Nice, we have a concept which evaluates to
false and calls
add with a given set of types. The last thing we have to do is to use
TestAdd together with our mocks inside a
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18
In 1 we test with
add works with built-in types but refuses
NoAdd, the mock without
operator+. Next, the rejection of mixed types is tested by 2. 1 already ensured as a side-effect that same types are permitted. Disallowing a parameter pack with less than 2 values is asserted by 3 and by that
add must be called with at least two parameters. 4 verifies that
operator+ must be
noexcept. Second last, 5 ensures that
operator+ returns an object of the same type, while 6 ensures that a valid class works. We are testing this already implicitly with other tests and is there for completeness only. That's it! We just tested the constraints of
add during compile-time with no other library or framework! I like that.
I hope you learned something about concepts and how to use them, but most and for all how to test them.
Concepts are a powerful new feature. While their main purpose is to add constraints to a function, they to also improve documentation and help us make constraints visible to users. With the technique I showed in this post you can ensure that your constraints are working as expected with just C++ utilities, of course at compile-time.
If you have other techniques or feedback, please reach out to me on Twitter or via email. In case, you like a more detailed introduction into Concepts tell me about it.
Please note, C++20 ships with a concept
same_as. This one here is a version which ignores cvref qualifiers and is a variadic version to retrieve the first type of a parameter pack. ↩