The idea of concepts is one of the major new features added to C++20. Concepts are an extension for templates. They can be used to perform compile-time validation of template arguments through boolean predicates. They can also be used to perform function dispatch based on properties of types.
With concepts, you can require both syntactic and semantic conditions. In terms of syntactic requirements, imagine that you can impose the existence of certain functions in the API of any class. For example, you can create a concept Car
that requires the existence of an accelerate
function:
1
2
3
4
5
6
#include <concepts>
template <typename C>
concept Car = requires (C car) {
car.accelerate()
};
Don’t worry about the syntax, we’ll get there next week.
Semantic requirements are more related to mathematical axioms, for example, you can think about associativy or commutativity:
1
2
a + b == b + a // commutativity
(a + b) + c == a + (b + c) // associativity
There are concepts in the standard library expressing semantic requirements. Take for example std::equality_comparable
.
It requires that
- the two equality comparison between the passed in types are commutative,
==
is symmetric, transitive and reflexive,- and
equality_comparable_with<T, U>
is modeled only if, given any lvalue t of typeconst std::remove_reference_t<T>
and any lvalue u of typeconst std::remove_reference_t<U>,
and let C bestd::common_reference_t<const std::remove_reference_t<T>&, const std::remove_reference_t<U>&>
,bool(t == u) == bool(C(t) == C(u))
.
Though this latter one is probably a bit more difficult to decipher. Anyway, if you are looking for a thorough article dedicated to semantic requirements, read this one by Andrzej Krzemieński.
The motivation behind concepts
We have briefly seen from a very high-level what we can express with concepts. But why do we need them in the first place?
For the sake of example, let’s say you want to write a function that adds up two numbers. You want to accept both integral and floating-point numbers. What are you going to do?
You could accept double
s, maybe even long double
s and return a value of the same type.
1
2
3
4
5
6
7
8
9
10
11
#include <iostream>
long double add(long double a, long double b) {
return a+b;
}
int main() {
int a{42};
int b{66};
std::cout << add(a, b) << '\n';
}
The problem is that when you call add()
with two int
s, they will be cast to long double
. You might want a smaller memory footprint, or maybe you’d like to take into account the maximum or minimum limits of a type. And anyway, it’s not the best idea to rely on implicit conversions.
Implicit conversions might allow code to compile that was not at all in your intentions. It’s not bad by definition, but implicit conversions should be intentional and not accidental.
In this case, I don’t think that an intentional cast is justified.
Defining overloads for the different types is another way to take, but it is definitely tedious.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
#include <iostream>
long double add(long double a, long double b) {
return a+b;
}
int add(int a, int b) {
return a+b;
}
int main() {
int a{42};
int b{66};
std::cout << add(a, b) << '\n';
}
Imagine that you want to do this for all the different numeric types. Should we also do it for combinations of long double
s and short
s? Eh… Thanks, but no thanks.
Another option is to define a template!
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
#include <iostream>
template <typename T>
T add(T a, T b) {
return a+b;
}
int main() {
int a{42};
int b{66};
std::cout << add(a, b) << '\n';
long double x{42.42L};
long double y{66.6L};
std::cout << add(x, y) << '\n';
}
If you have a look at CPP Insights you will see that code was generated both for an int
and for a long double
overload. There is no static cast taking place at any point.
Are we good yet?
Unfortunately, no.
What happens if you try to call add(true, false)
? You’ll get a 1
as true
is promoted to an integer, summed up with false
promoted to an integer and then they will be turned back (by static_cast
) into a boolean.
What if you add up two string? They will be concatenated. But is that really what you want? Maybe you don’t want that to be a valid operation and you prefer a compilation failure.
So you might have to forbid that template specialization. And for how many types do you want to do the same?
What if you could simply say that you only want to add up integral or floating-point types. In brief, rational numbers. And here come concepts
into the picture.
With concepts, you can easily express such requirements on template parameters.
You can precise requirements on
- the validity of expressions (that certain functions should exist in the class’ API)
- the return types of certain functions
- the existence of inner types, of template specializations
- the type-traits of the accepted types
How? That’s what we are going to explore in this series on C++ concepts.
What’s next?
During the next couple of weeks we are going to discuss:
- how to use concepts with functions
- how to use concepts with classes
- what kind of predefined concepts the standard library introduced
- how to write our own concepts (part I and part II)
- C++ concepts in real life
- C++ Concepts and logical operators
- Multiple destructors with C++ concepts
- C++ Concepts and the Core Guidelines
Stay tuned!
If you want to learn more details about C++ concepts, check out my book on Leanpub!