Your own concepts

By , last updated September 3, 2019

The easiest concept is probably the boolean concepts true and false.

They will either accept everything, or reject anything. After all, concepts are about accepting or rejecting a type or a class.

struct A{};

// Concept requires is true
template<typename T>
requires true
void accept_all(T t){}

// Concept requires is false
template<typename T>
requires false
void accept_none(T t){}

accept_all(a);  // Ok
accept_none(a); // Fail

As we’ve seen before, the errors are to the point and there is no doubt what went wrong.

concept-true.cpp: In function 'int main()':
concept-true.cpp:26:15: error: cannot call function 'void accept_none(T) [with T = A]'
  accept_none(a);
               ^
concept-true.cpp:11:6: note:   constraints not satisfied
 void accept_none(T t){}
      ^~~~~~~~~~~
concept-true.cpp:11:6: note:   'false' evaluated to false

Using such a simple expression as requires true or requires false have no immediate value except in special cases.

But such simple cases are perfect as a learning tool. They are easy to understand and easy to grasp.

Next is defining a concept, which can be used elsewhere.

The concept is on or off

Using a requires-section for all template would be code duplication, and we don’t like that. If the requirements for a certain concept has changed, you should be able to edit the concept in once place only. The compiler will take care of the concepts inheriting from this particular concept. There are concepts, which have complex requirements. One example could be a Stack concept. A Stack must be able to support push, pop, and top.

Continuing the spirit of simple examples before complex examples. Here are named variable concepts. They can be used elsewhere, either in a requires clause or when there is only one type, or directly in the template argument type list in-place of typename or class.

template<typename T> concept bool C_TRUE = true;    // Concept true
template<typename T> concept bool C_FALSE = false;  // Concept false

The following code listing both show one short form and one longer form of both concepts.

// True concept, short form
template<C_TRUE T> void fn_true_1(){}

// Longer form of the above
template<typename T> requires C_TRUE<T> void fn_true_2(){}

// False concept, short form
template<C_FALSE T> void fn_false_3(){}

// Longer form of the above
template<typename T> requires C_FALSE<T> void fn_false_4(){}

The following instantiations will work and fail as expected.

fn_true_1<A>();     // Ok
fn_true_2<A>();     // Ok
fn_false_3<A>();    // Fail
fn_false_4<A>();    // Fail

Chained requirements

Programmers hate writing code more than once. A mantra is to keep units small and let them do one thing, and do it well. With complex requirements, it’s possible to write concepts with full requirements, or better yet, have many small and easy concepts and build complex concepts from the building blocks.

An overview of what we want to do is to define concept A with implementation-of-A, concept B with implementation-of-B, and so on.

To build complex concept C, it’s possible to combine concept A and B into C. You should not implement concept C with implementation-of-A and implementation-of-B. Any bugs and updates in A and B will not propagate automatically.

As a basis for this, we’ll start by implementing a concept for iterators.

Incrementable and Decrementable

A ForwardIterator and a ReverseIterator must support incrementing and decrementing. Increment and Decrement will be two separate concepts, which we’ll use as building blocks.

Two variable concepts named Incrementable and Decrementable are defined by:

template<typename T>
concept bool Incrementable = requires(T t) { ++t; t++; };

template<typename T>
concept bool Decrementable = requires(T t) { --t; t--; };

Any type supporting post and pre increment and post and pre decrement will be allowed.

Dereferencable

An iterator is just a lightweight object pointing at another object. To access the pointed-to-object, you must dereference the iterator just like you would do with a pointer.

To check if the type is dereferencable, use the unary pointer deference operator *t.

template<typename T>
concept bool Dereferencable = requires(T t) { *t; };

Any type supporting dereferencing are allowed.

Iterator concept

When combining concepts to make a compound concept, it’s called a conjunction. Don’t let that put you off, it is only another word for what would best be described as A && B in C++.

With the Standard Template Library (STL), there are five types of iterators. Those are Input, Output, Forward, Bidirectional and RandomAccess. For iterators, there are more requirements than being able to increment, decrement, and dereference. Make a search for c++ iterator types in your favorite search engine for more information about the real requirements. Those are out of the scope of this chapter.

If our algorithm only requires a forward iterator, we can require a forward iterator.

Using the previous defined concepts, we can construct a FwdIterator (InputIterator) concept by combining Incrementable and Dereferencable.

template<typename T>
concept bool FwdIterator = Incrementable<T> && Dereferencable<T>;

An example for an algorithm only requiring a forward iterator is find. Using the concept FwdIterator will limit the use of the find algorithm only accessible for FwdIterator types. If you use an other incompatible type, the compiler will tell you exactly what is wrong and why.

template<typename T>
FwdIterator find (FwdIterator first, FwdIterator last, const T& value)
{
    while (first != last && *first != value) 
       ++first;
    return first;
}

Professional Software Developer, doing mostly C++. Connect with Kent on Twitter.