A brief introduction to Concepts – Part 2

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.

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

Glennan Carnie
Dislike (0)
Website | + posts

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.

About Glennan Carnie

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.
This entry was posted in C/C++ Programming and tagged , , , , , , , . Bookmark the permalink.

4 Responses to A brief introduction to Concepts – Part 2

  1. rboc says:

    The link to part 1 at the beginning is broken

    Like (0)
    Dislike (0)
  2. Fixed now! Thanks!

    Like (0)
    Dislike (0)
  3. Attila Krüpl says:

    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!

    Like (0)
    Dislike (0)
  4. 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

    Like (1)
    Dislike (0)

Leave a Reply