[GRCPP] Introduction to concepts (C++20)

DimitriosPlatis 35 views 35 slides May 11, 2024
Slide 1
Slide 1 of 35
Slide 1
1
Slide 2
2
Slide 3
3
Slide 4
4
Slide 5
5
Slide 6
6
Slide 7
7
Slide 8
8
Slide 9
9
Slide 10
10
Slide 11
11
Slide 12
12
Slide 13
13
Slide 14
14
Slide 15
15
Slide 16
16
Slide 17
17
Slide 18
18
Slide 19
19
Slide 20
20
Slide 21
21
Slide 22
22
Slide 23
23
Slide 24
24
Slide 25
25
Slide 26
26
Slide 27
27
Slide 28
28
Slide 29
29
Slide 30
30
Slide 31
31
Slide 32
32
Slide 33
33
Slide 34
34
Slide 35
35

About This Presentation

Welcome the the next GRCPP meetup where we will introduce a powerful new feature in C++20: Concepts.

Concepts are a way to specify requirements on template parameter types and to make templates more expressive and easier to understand..
We will show how to use concepts to create "interfaces&qu...


Slide Content

concepts (since C++20)
Create "interfaces" for your templates
platis.solutions © for GRCPP Meetup

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: [email protected]
platis.solutions © for GRCPP Meetup

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

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

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

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

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

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

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

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

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

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

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

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

"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

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

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

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

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

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

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

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

Specializing member functions
template<typename Motor>
concept HasOdometer = requires(Motor m) {
m.getPulses();
};
template<typename Motor>
struct Car {
void drive() { std::cout << "Drive\n"; }
void drive() requires HasOdometer<Motor> {
std::cout << "Drive with cruise control\n";
}
};
The compiler chooses the most specialized member function.
The trailing requires clause becomes very useful here.
platis.solutions © for GRCPP Meetup

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 function\n";
} else if constexpr (requires { value.bar(); }) {
std::cout << "T has bar() member function\n";
} else {
std::cout << "T has neither foo(int) nor bar() member functions\n";
}
}
Create concepts on the fly with if constexpr and requires.
We may specify arguments in the requires clause.
platis.solutions © for GRCPP Meetup

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

(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

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

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

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

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

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

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

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

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

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