- 1. concepts (since C++20) Create "interfaces" for your templates platis.solutions © for GRCPP Meetup
- 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