More Related Content Similar to [GRCPP] Introduction to concepts (C++20) (20) More from Dimitrios Platis (11) [GRCPP] Introduction to concepts (C++20)2. About me
Grew up in Rodos, Greece
Software Engineering at GU & Chalmers
Working with embedded systems
Teaching
DIT113, DAT265, Thesis supervision
C++, Coursera, Udemy
Open source projects
https://platis.solutions
https://github.com/platisd
Email: dimitris@platis.solutions
platis.solutions
©
for
GRCPP
Meetup
3. Requirements on template arguments
C++20 introduces constraints
Specify requirements on template arguments
Seamless selection of the appropriate overload or specialization
Named sets of such requirements are called concepts
Constraints and concepts
requires expression
platis.solutions
©
for
GRCPP
Meetup
4. Why do we need concepts?
template <typename Camera>
class AutonomousCar {
Camera mCamera;
public:
// ... A lot of code
};
How do we ensure that Camera has all required functions?
Normally, we read the Camera interface, with templates we can't.
platis.solutions
©
for
GRCPP
Meetup
5. Why do we need concepts?
template <typename T>
T getMedianNumber(std::vector<T> values) {
std::sort(values.begin(), values.end());
return values[values.size() / 2];
}
What kind of types does it make sense for getMedian accept?
What kind of types can getMedian accept?
platis.solutions
©
for
GRCPP
Meetup
6. Let's make our getMedianNumber more explicit
template<typename T>
T getMedianNumber(std::vector<T> values) {
static_assert(std::is_integral_v<T> || std::is_floating_point_v<T>,
"T must be an integral or floating-point");
std::sort(values.begin(), values.end());
return values[values.size() / 2];
}
static_assert is great, but can make things more readable?
platis.solutions
©
for
GRCPP
Meetup
7. Let's make our own constraint
template<typename T>
requires std::integral<T> || std::floating_point<T>
// requires std::is_integral_v<T> || std::is_floating_point_v<T>
T getMedianNumber(std::vector<T> values) {
std::sort(values.begin(), values.end());
return values[values.size() / 2];
}
std::vector<std::string> files{"file22.txt", "file11.txt", "file33.txt"};
std::cout << getMedianNumber(files) << std::endl; // Compilation error
std::vector numbers{0, 9, 5, 7, 3, 6, 2, 8, 1, 4, 10};
std::cout << getMedianNumber(numbers) << std::endl; // 5
"No operand of the disjunction is satisfied"
requires std::integral<T> || std::floating_point<T>
platis.solutions
©
for
GRCPP
Meetup
8. Let's make our own concept
template<typename T>
concept Number = std::integral<T> || std::floating_point<T>;
// concept Number = std::is_integral_v<T> || std::is_floating_point_v<T>;
template<typename T>
requires Number<T>
T getMedianNumber(std::vector<T> values) {
std::sort(values.begin(), values.end());
return values[values.size() / 2];
}
Something that is satisfied or not, often treated "like a boolean"
Use concepts in requires clauses or to compose other concepts
std::integral and std::floating_point are built-in concepts
platis.solutions
©
for
GRCPP
Meetup
9. Trailing requires syntax is also possible:
template<typename T>
T getMedianNumber(std::vector<T> values)
requires Number<T> // <--- Trailing requires
{
std::sort(values.begin(), values.end());
return values[values.size() / 2];
}
It is exactly the same as the previous example:
template<typename T>
requires Number<T> // <--- Leading requires
T getMedianNumber(std::vector<T> values) {
std::sort(values.begin(), values.end());
return values[values.size() / 2];
}
platis.solutions
©
for
GRCPP
Meetup
10. Let's make getMedianNumber more readable
template<typename T>
concept Number = std::integral<T> || std::floating_point<T>;
template<Number T>
T getMedianNumber(std::vector<T> values) {
std::sort(values.begin(), values.end());
return values[values.size() / 2];
}
Use a concept as a non-type template parameter
Highly expressive and readable
Constrain the template parameter is a way that feels intuitive
platis.solutions
©
for
GRCPP
Meetup
11. concept vs requires
template<Number T>
requires std::is_integral_v<T> || std::is_floating_point_v<T>
T getMedianNumber1(std::vector<T> values) { /* ... */ }
template<typename T>
requires Number<T>
T getMedianNumber2(std::vector<T> values) { /* ... */ }
requires used to express requirements on template arguments
A concept is a named set of requirements
A concept is to a requires what a function is to a statement
In getMedianNumber2 we named the requirements Number
platis.solutions
©
for
GRCPP
Meetup
12. How do you requires ?
template<typename T>
concept Motor = requires(T m) { // <--- `requires` with curly braces
m.start();
m.stop();
};
template<typename T>
requires Motor<T> // <--- `requires` without curly braces
class Car {
// ...
};
Without curly braces: requires <some boolean expression>
With curly braces: requires(T m) { statements...; }
platis.solutions
©
for
GRCPP
Meetup
13. requires without curly braces
template<typename T>
requires std::is_constructible_v<T, std::string, int>
void createWithStringAndInt() { /* ... */ }
Expects a boolean expression to follow
If the boolean expression is true , requires is satisfied and valid
If the expression is false , requires is ill-formed
No error is generated if requires is ill-formed
May also be with parentheses: requires ( ... )
platis.solutions
©
for
GRCPP
Meetup
14. requires with curly braces
template<typename T>
concept StringAndIntConstructible = requires(std::string s, int i) {
T{s, i};
};
Expects a block of statements to follow {within curly braces}
Optionally preceded by objects for statement formulation
After type substitution if statements valid, requires is true
If any statement is ill-formed, requires evaluates to false
No error is generated if any statement is ill-formed
platis.solutions
©
for
GRCPP
Meetup
15. "Interfaces" for our template types
struct Motor {
Motor(int directionPin, int speedPin);
bool start();
bool stop();
};
class Car {
Motor mMotor{5 /* directionPin */, 10 /* speedPin */};
public:
void drive();
};
If Car was to become a template with Motor as a template type
would we ensure that Motor has start and stop functions?
platis.solutions
©
for
GRCPP
Meetup
16. Template "interfaces" without concepts: SFINAE
template<typename T, typename = void>
struct IsMotor : std::false_type {};
template<typename T>
struct IsMotor<T, std::void_t<decltype(std::declval<T>().start()),
decltype(std::declval<T>().stop())>>
: std::true_type {};
template<typename Motor>
class Car {
static_assert(IsMotor<Motor>::value, "Motor needs start and stop");
Motor mMotor;
public:
void drive();
};
platis.solutions
©
for
GRCPP
Meetup
17. Template "interfaces" with concepts
template<typename T>
concept Motor = requires(T m) {
T{int{}, int{}}; // Constructible with two ints
m.start(); // T has a public start method
m.stop(); // T has a public stop method
};
template<Motor M>
class Car {
M mMotor{5 /* directionPin */, 10 /* speedPin */};
public:
void drive();
};
Much simpler? Let's look at the requires expression.
platis.solutions
©
for
GRCPP
Meetup
18. requires as a "contract"
template <typename T>
concept Motor = requires(T m) {
m.start();
m.stop();
};
Evaluates to true if the expression is valid after substitution
false otherwise but no error is generated if ill-formed
Every line is a new "term" in the "contract", all must be satisfied
Do not see them as "commands" but as "terms in a contract"
Full syntax: cppreference.com/w/cpp/language/requires
platis.solutions
©
for
GRCPP
Meetup
19. requires requiring...
template <typename T>
concept Gyroscope = requires(T g, std::vector<int> params, int frequency) { // 1
T{params}; // 2
g.calibrate(); // 3
{ g.getAngle() } -> std::same_as<double>; // 4
g.setFrequency(frequency); // 5
};
1. "Objects" needed to express the requirements/statements
2. A constructor accepting a std::vector<int>
3. A calibrate() member function existing (return type unchecked)
4. getAngle() member function returning double
5. setFrequency(int) member function accepting an int
platis.solutions
©
for
GRCPP
Meetup
20. Verify getAngle that returns double exists with SFINAE:
template<typename T, typename = void>
struct HasGetAngle : std::false_type {};
template<typename T>
struct HasGetAngle<
T, std::enable_if_t<std::is_same<
double, decltype(std::declval<T>().getAngle())>::value>>
: std::true_type {};
// Alternatively:
// template<typename T>
// struct HasGetAngle<T, std::void_t<decltype(std::declval<T>().getAngle())>>
// : std::bool_constant<std::is_same<
// double, decltype(std::declval<T>().getAngle())>::value> {};
This is a lot of boilerplate code for a "simple" check.
platis.solutions
©
for
GRCPP
Meetup
21. More requires
template<typename T>
concept MyBigConcept = requires(T a, T b, std::ostream& out) {
a + b; // Addable with its own type
a++; // Incrementable
{ a == b } -> std::same_as<bool>; // Equality comparable
typename T::inner; // T::inner is a type (exists)
{ out << a } -> std::same_as<std::ostream&>; // Streamable to std::ostream
requires std::integral<typename T::value_type>; // T::value_type satisfies std::integral
{ a.size() } -> std::integral; // Return type satisfies other concept
{ T::Instances } -> std::same_as<std::size_t>; // T::Instances static and std::size_t
a.id; // `id` is a public member variable
};
platis.solutions
©
for
GRCPP
Meetup
22. Choosing the right candidate
template<typename Robot>
void handleEnemies(Robot) { std::cout << "I surrender!n"; }
template<typename Robot>
requires HasBullets<Robot>
void handleEnemies(Robot r) { r.shootBullets(); }
template<HasMissiles Robot>
void handleEnemies(Robot r) { r.shootMissiles(); }
struct RobotA { void shootBullets() { std::cout << "Bang!n"; } };
struct RobotB { void shootMissiles() { std::cout << "Shooosh!n"; } };
struct RobotC {};
handleEnemies(RobotA{}); // "Bang!"
handleEnemies(RobotB{}); // "Shooosh!"
handleEnemies(RobotC{}); // "I surrender!"
platis.solutions
©
for
GRCPP
Meetup
23. Specializing member functions
template<typename Motor>
concept HasOdometer = requires(Motor m) {
m.getPulses();
};
template<typename Motor>
struct Car {
void drive() { std::cout << "Driven"; }
void drive() requires HasOdometer<Motor> {
std::cout << "Drive with cruise controln";
}
};
The compiler chooses the most specialized member function.
The trailing requires clause becomes very useful here.
platis.solutions
©
for
GRCPP
Meetup
24. if constexpr and requires
template<typename T>
void print_info(T value) {
if constexpr (requires(int i) { value.foo(i); }) {
std::cout << "T has foo(int) member functionn";
} else if constexpr (requires { value.bar(); }) {
std::cout << "T has bar() member functionn";
} else {
std::cout << "T has neither foo(int) nor bar() member functionsn";
}
}
Create concepts on the fly with if constexpr and requires .
We may specify arguments in the requires clause.
platis.solutions
©
for
GRCPP
Meetup
25. What will be printed out?
template<typename T>
constexpr void print_type_info(const T& value) {
if constexpr (requires { std::is_integral_v<T>; }) {
std::cout << "Value is integral: " << value << std::endl;
} else {
std::cout << "Value is not integral" << std::endl;
}
}
print_type_info(5);
print_type_info(3.14);
print_type_info("Hello");
"Value is integral..." 3 times. Why?
Curly-braced requires becomes true if statements are valid
platis.solutions
©
for
GRCPP
Meetup
26. (Avoid) Concepts that are always satisfied
template<typename T>
concept AlwaysSatisfied1 = true;
template<typename T>
concept AlwaysSatisfied2 = requires { false; };
template<typename T>
concept AlwaysSatisfied3 = requires(T t) {
std::is_integral_v<T>;
std::is_floating_point_v<T>;
};
static_assert(AlwaysSatisfied1<int>); // Hardcoded to true
static_assert(AlwaysSatisfied2<int>); // `false;` is a valid statement
static_assert(AlwaysSatisfied3<int>); // `true;` and `false;` are valid
platis.solutions
©
for
GRCPP
Meetup
27. Which of the following constraints are always satisfied?
template<typename T>
concept Integral = requires {
std::integral<T>; // 1
requires std::integral<T>; // 2
std::is_integral_v<T>; // 3
{ T{} } -> std::integral; // 4
};
std::integral<T> always a valid expression ( true or false )
requires std::integral<T> becomes invalid if T is not integral
std::is_integral_v<T> always a valid expression ( true or false )
{ T{} } -> std::integral becomes invalid if T is not integral
platis.solutions
©
for
GRCPP
Meetup
28. requires { requires <true|false> }
template<typename T>
constexpr void print_type_info(const T& value) {
if constexpr (requires { requires std::is_integral_v<T>; }) {
std::cout << "Value is integral: " << value << std::endl;
} else {
std::cout << "Value is not integral" << std::endl;
}
}
requires without curly braces becomes valid if expression is true
requires std::is_integral_v<T>; is ill-formed if T not integral
requires with curly braces evaluates to true for valid statements
requires { ... }; is false if nested requires is ill-formed
platis.solutions
©
for
GRCPP
Meetup
29. requires requires { statements...; }
template<typename Container>
requires requires(Container a, Container::value_type v1, Container::value_type v2) {
{ a.begin() } -> std::input_iterator;
{ a.end() } -> std::sentinel_for<decltype(a.begin())>;
{ a.size() } -> std::same_as<std::size_t>;
{ v1 < v2 } -> std::same_as<bool>;
}
void print_sorted(Container& c) { /* ... */ }
requires with curly braces checks if the statements are valid
Becomes true if all statements are valid, false otherwise
requires without curly braces checks if the expression is true
Becomes valid if the expression is true , ill-formed otherwise
platis.solutions
©
for
GRCPP
Meetup
30. Concepts with multiple types
template<typename Motor, typename Odometer>
concept CompatibleOdometry = requires(Motor m, Odometer o) {
m.attach(o);
};
template<typename Motor, typename Odometer>
requires CompatibleOdometry<Motor, Odometer>
class Smartcar {
public:
Smartcar(Motor left, Motor right, Odometer odometer) { /* ... */ }
};
CompatibleOdometry requires Motor and Odometer to be compatible
Motor with member function attach accepting Odometer
platis.solutions
©
for
GRCPP
Meetup
31. Concepts with lambdas (no template parameter
list)
template<typename Car>
concept CanStop = requires(Car car) { car.stop(); };
template<typename Sensor>
concept CanDetectObstruction = requires(Sensor sensor) { sensor.isObstructed(); };
auto stopIfObstructed = [](auto& car, auto& sensor) -> void
requires CanDetectObstruction<decltype(sensor)> && CanStop<decltype(car)>
{
if (sensor.isObstructed()) { car.stop(); }
};
requires goes after the (optional) trailing return type
platis.solutions
©
for
GRCPP
Meetup
32. Concepts with lambdas (no template parameter
list)
auto stopIfObstructed2 = [](auto& car, auto& sensor)
requires requires {
{ sensor.isObstructed() } -> std::convertible_to<bool>;
car.stop();
}
{
if (sensor.isObstructed()) { car.stop(); }
};
Requirements on the fly with requires requires { statements...; }
platis.solutions
©
for
GRCPP
Meetup
33. Concepts with lambdas (template parameter list)
auto stop1 = []<typename Car, typename Sensor>
requires CanDetectObstruction<Sensor> && CanStop<Car>
(Car & car, Sensor & sensor) {
if (sensor.isObstructed()) { car.stop(); }
};
auto stop2 = []<CanStop Car, CanDetectObstruction Sensor>(Car& c, Sensor& s) {
if (s.isObstructed()) { c.stop(); }
};
requires after the lambda template parameter list
Can also go after the (optional) trailing return type
Concepts as non-type template parameters
platis.solutions
©
for
GRCPP
Meetup
34. Takeaways
Concepts are named sets of requirements on template types
concept is to requires what a function is to a statement
Two types of requires which can be confusing:
requires without curly braces
Expects a boolean expression, evaluates to valid or ill-formed
requires with curly braces
Expects a block of statements, evaluates to true or false
platis.solutions
©
for
GRCPP
Meetup
35. Takeaways
Concepts simplify code and provide better error messages
Use concepts to create "interfaces" for template classes & methods
Skip reading code or compiler errors to find the right type to use
Avoid cryptic and verbose SFINAE constructs
static_assert is still useful for providing custom error messages
platis.solutions
©
for
GRCPP
Meetup