In part 1 of this article we looked at adding requirements to parameters in template code to improve the diagnostic ability of the compiler. (I’d recommend reading this article first, if you haven’t already)
Previously, we looked at a simple example of adding a small number of requirements on a template parameter to introduce the syntax and semantics. In reality, the constraints imposed on a template parameter could consist of any combination of
- Type traits
- Required type aliases
- Required member attributes
- Required member functions
Explicitly listing all of this requirements for each template parameter, and every template function / class gets onerous very quickly.
To simplify the specification of these constraints we have Concepts.
Contents
Concepts
A Concept defines a specification for the set of constraints that must be supported by a particular template type. It is compelling, on first introduction, to think of a Concept as a meta-type (that is, a type that describes a type), but that’s not an accurate analogy. Think, instead, of a Concept describing the semantics of a set of types; or the characteristics a set of types may exhibit.
Concepts are written as template definitions, specifying a block of requirements on a particular type.
template <typename T> concept bool Bufferable = requires(T) { requires std::is_default_constructible<T>::value; requires std::is_copy_assignable<T>::value; requires std::is_copy_constructible<T>::value; };
We’ve collected together our set of requirements from previously into a named – reusable – entity.
Note the ‘type’ of concept – bool. In fact, all Concepts are of type bool, so could have been omitted; the bool was left in since all definitions in C++ must have a type!
We can now apply this Concept to our Buffer class
template <typename T, size_t sz> requires Bufferable<T> class Buffer { public: void push(const T& in_val); T pop(); bool empty() const; private: using Container = std::array<T, sz>; using Iterator = typename Container::iterator; Container buffer { }; Iterator write { std::begin(buffer) }; Iterator read { std::begin(buffer) }; size_t num_items { 0 }; };
In reality, this example is a pretty poor example of a Concept. Being ‘Bufferable’ isn’t really a characteristic of types, or even a well-defined semantic. I’ll leave the example as is:
- As an example of the syntax
- Because I can’t think of a better example at the moment
- There are plenty of better examples you’ll now be able to appreciate
Let’s explore a different example to see how else we might use Concepts. The Scope-locked idiom exploits RAII to ensure resource locks (mutexes, semaphores, etc) are unlocked correctly when a function is exited to avoid the possibility of deadlock. Below is a simple implementation
template <typename T> class Scope_lock { public: Scope_lock(const T& lockable) : lock { lockable } { lock.lock(); // <= Lock on construction } ~Scope_lock() { lock.unlock(); // <= Unlock on destruction } private: T lock; };
In this case, in order to be used with the Scope_lock the template type must support two methods: lock() and unlock().
We can define this constraint within a Concept.
template <typename T> concept bool Lockable = requires(T t) { { t.lock() } -> void; { t.unlock() } -> void; };
Note we are using an ‘instance’ (but not really, since the template is not instantiated into any run-time code) to establish the set of methods T must support.
As before, we can use this to constrain the template parameter of the Scope_lock
template <typename T> requires Lockable<T> class Scope_lock { public: Scope_lock(const T& lockable) : lock { lockable } { lock.lock(); } ~Scope_lock() { lock.unlock(); } private: T lock; };
Given the following mutual exclusion mechanisms
class Mutex { public: void lock(); bool try_lock(); void unlock(); }; class Recursive_mutex { public: void lock(); bool try_lock(); void unlock(); }; class Semaphore { public: void give(); void take(); };
We get the (expected) results from the compiler
int main() { Mutex mutex { }; Recursive_mutex rec_mutex { }; Semaphore semphr { }; Scope_lock<Mutex> lock1 { mutex }; // OK Scope_lock<Recursive_mutex> lock2 { rec_mutex }; // OK Scope_lock<Semaphore> lock3 { semphr }; // FAIL }
src/main.cpp: In function 'int main()': src/main.cpp:179:25: error: template constraint failure Scope_lock<Semaphore> lock3 { semphr }; // FAIL ^ src/main.cpp:179:25: note: constraints not satisfied src/main.cpp:95:14: note: within 'template<class T> concept const bool Lockable<T> [with T = Semaphore]' concept bool Lockable = ^~~~~~~~ src/main.cpp:95:14: note: with 'Semaphore t' src/main.cpp:95:14: note: the required expression 't.lock()' would be ill-formed src/main.cpp:95:14: note: the required expression 't.unlock()' would be ill-formed
(As an aside, I could’ve used template type-deduction on the Scope_locks. Explicit instantiation (at least in gcc) gives a more succinct error message)
Specifying interfaces with Concepts rather than types
C++ provides syntactic sugar for Concept-constrained types which allows us to be expressive without the burden of complex syntax.
The class
template <typename T> requires Lockable<T> class Scope_lock { // ... };
Can be re-written as
template <Lockable T> class Scope_lock { // ... };
This expresses that the template parameter T must support the characteristics / semantics of the concept ‘Lockable’.
This doesn’t just apply to template classes, but also functions, too. For example
template <typename T> requires Lockable<T> void lock(const T& in_val);
Can be re-written as
template <Lockable T> void lock(const T& in_val);
or even
void lock(const Lockable& in_val);
This additional syntax allows us to write expressive and well-defined interfaces.
// Supports any type // template <typename T> void process_any(const T& in); // Supports any type that fulfils the Lockable concept // void process_some(const Lockable& in); // Only supports Mutex (or sub-types) // void process_one(const Mutex& in); int main() { Mutex mtx { }; process_any(mtx); // OK process_some(mtx); // OK process_one(mtx); // OK Recursive_mutex rec_mtx { }; process_any(rec_mtx); // OK process_some(rec_mtx); // OK process_one(rec_mtx); // FAIL Semaphore semphr { }; process_any(semphr); // OK process_some(semphr); // FAIL process_one(semphr); // FAIL }
In this example, the generic function process_any() will take any of our objects, as we might expect. Mutex objects support the Concept of Lockable; and (of course) are Mutex objects, so the call to process_one() works. Recursive_mutex objects are Lockable, but aren’t Mutex objects (or derived from them, in this case).
We’re not even limited to using Concepts as function parameters. They can also be used for object definitions. This gives us an (superior) alternative to automatic type-deduction. For example.
// Returns some lock-type entity // auto get_current_lock(); int main() { Mutex mtx1 = get_current_lock(); // Explicit auto mtx2 = get_current_lock(); // Don't care Lockable mtx3 = get_current_lock(); // Anything Lockable // ... }
- In the case of mtx1, we explicitly want a Mutex object. Any other type will fail.
- In the case of mtx2, we are indifferent to the type of the object returned. This is the least explicit option. We could get any type back from the function, and as long as the code compiles and runs correctly, that’s fine.
- In the case of mtx3, we are not concerned with the absolute type of the object returned, but rather its characteristics. That is, we are specifying the semantics / capabilities we need, rather than the type.
C++ has typically favoured option 1; using inheritance to deal with sub-typing. Option 2 was introduced with C++11; and in many ways is the least desirable option (saying “I don’t care” is never a satisfying response in software design!)
Option 3 gives us the flexibility to build generic code but still be able to concretely specifying the semantics of the objects in the system.
Overloading concept functions
If we can write Concept-based functions what happens when we overload them? For example, let’s establish some Concepts first
template <typename T> concept bool Equality_comparable = requires(T a, T b) { { a == b } -> bool; { a != b } -> bool; }; template <typename T> concept bool Value_comparable = requires(T a, T b) { { a == b } -> bool; { a != b } -> bool; { a < b } -> bool; { a > b } -> bool; };
(These aren’t necessarily the complete set of requirements for these concepts, but they’re adequate for this example)
We can now write overloaded functions based on these concepts
bool process(const Equality_comparable& x, const Equality_comparable& y) { cout << "Equality_comparible" << endl; return (x == y); } bool process(const Value_comparable& x, const Value_comparable& y) { cout << "Value_comparible" << endl; return (x > y); } int main() { process(10, 20); // Which overload is called? }
When presented with overloads the compiler will select the function that is the most restrictive. That is, has the most constrained Concept. Since integers support all the semantics of Value_comparable that is the function that is selected. In this case Value_comparable is a superset of Equality_comparable.
We can see this better by defining two user-defined types – one that is only Equality_comparable, the other that is Value_comparable
// Waypoints can be compared for equality but it // makes no sense to say one Waypoint is // greater/less than another // class Waypoint { public: Waypoint(double lat, double lon); // Public API... bool operator==(const Waypoint& rhs) const; bool operator!=(const Waypoint& rhs) const; private: // Implementation... }; // Pressure values can be compared for equality // as well as greater/less than. // class Pressure { public: Pressure(double init); // Public API... bool operator==(const Pressure& rhs) const; bool operator!=(const Pressure& rhs) const; bool operator>(const Pressure& rhs) const; bool operator<(const Pressure& rhs) const; private: // Implementation... }; int main() { Waypoint point_A { 90.0, 77.6 }; Waypoint point_B { 67.7, 77.4 }; process(point_A, point_B); // => "Equality_comparable" Pressure upstream { 88.2 }; Pressure downstream { 77.2 }; process(upstream, downstream); // => "Value_comparable" }
Summary
As I wrote at the beginning of this article, we’ve barely scratched the surface of the capabilities of Concepts in C++. My goal with this article was to give you the basic syntax and application. I highly recommend going and reading Bjarne Stroustrup’s white paper on Concepts for a for more in-depth exploration of these ideas.
Their ability to allow comprehensive, well-defined and expressive interfaces could make a huge difference to both library and client code.
If you’d like to learn more about C++ – in particular its application in embedded systems – we recommend the following training courses
C++11-501 – Modern C++ for Embedded Developers
C++11-502 – Real-Time Modern C++
AC++11-401 – Transitioning to C++11/C++14
C++-501 – C++ for Embedded Developers (C++03)
C++-502 – Real-Time C++ (C++03)
AC++-501 – Advanced C++ for Embedded Systems(C++-03)
DP-403 – Design patterns in C++
OO-504 – Real-Time Software Design with UML
- Practice makes perfect, part 3 – Idiomatic kata - February 27, 2020
- Practice makes perfect, part 2– foundation kata - February 13, 2020
- Practice makes perfect, part 1 – Code kata - January 30, 2020
Glennan is an embedded systems and software engineer with over 20 years experience, mostly in high-integrity systems for the defence and aerospace industry.
He specialises in C++, UML, software modelling, Systems Engineering and process development.
The link to part 1 at the beginning is broken
Fixed now! Thanks!
Hi Glennan, the links of the recommended training courses don't seem to work. Can you please look into this? I'd love to dig into a couple of them. Thanks!
Hi Attila,
Thanks for letting us know. We're looking into the problem now.
In the meantime you can find all our courses here: https://www.feabhas.com/course-list