C++20 - An Introduction to Concepts

An Introduction to Concepts in C++20

Subscribe to my newsletter and never miss my upcoming articles

C++ 20 is here πŸŽ‰πŸŽ‰πŸŽ‰

C++20 is the latest approved ISO standard for C++. It unanimously passed its final technical approval ballot on September 4, 2020. It is a big change with a lot of features, and will greatly change the way C++ is written just like C++11 did almost 9 years ago.

The biggest 4 additions in this version of the standard are

  • Concepts
  • Modules
  • Ranges
  • Coroutines

This article aims to introduce the reader to the idea of concepts, what they are and how to implement them.

Almost all of the C++20 features are available in major compilers. You can check up the compiler support for the individual features on this page

Templates and Generic Programming in C++

The idea of Generic Programming is that, in a typed language developers should be able to create functions/classes for generic types, i.e we can write a function or a class for any type T and we can specify T later. Using generic code helps avoid function overloading and dramatically increases code re-usability.

Generics is implemented in C++ using Templates. The following is an example of a generic Container class that can hold a collection of items of type T

template <typename T> 
class Container {
private:
    int m_size;
    T *data;
public:
    constexpr Container() = default;
    constexpr Container(int size) { data = new T[size]; }

    T &operator[](const int &i) {
        if (i < m_size) return data[i];
    }

    ~Container() { delete[] data; }
};

Now we can use this templated Container class like this,

Container<int> container_int {10}; // T=int, will hold 10 integer values
Container<std::string> container_str {5}; // T=std::string, will hold 5 strings

It is important to have a good understanding of templates to get the whole point of concepts, they build on top of templates and give developers the capability to design really robust generic code.

Note: The syntax and library functions for concepts are fully available in GCC 10. All the examples mentioned below are compiled using g++-10 on Ubuntu 20.04

What are Concepts ?

If you notice, there is a slight issue with templates, the issue is that they are 100% generic, T could literally be any type.

Imagine we wrote a template sort function in a library but we implemented it for any type of numeric array, as there is no way to limit the kind of Ts our sort function can accept, someone could by mistake pass an array of images and will have to later deal with a lot of errors.

For situations like these we need features that could help us constrain the template parameters, and that is exactly the problem concepts in C++20 intend to solve.

Concepts can be used to impose constraints on template parameters at compile time. They are compile time predicates that we can implement to make sure the T passed in actually fulfils some sort of "criteria" for successful execution.

concept and requires

concept and requires are the two important keywords we use to implement concepts.

A concept is just a set of named requirements specified using the requires keyword.

template <typename T> 
concept Incrementable = requires(T x) {
    x++;
    ++x;
};
  • The first line specifies the concept declared below to be templated for any type T (concepts are always templated)
  • On the second line we declare the concept "Incrementable"
  • After that we specify the constraint requirements, in this case, we check that for any T x, the statements x++ and ++x should compile.

Anything you specify in the requires clause just means that those statements should be valid code that can compile successfully. We do not care about what values those statements will return. Once we have declared our concept, we have to apply it on templated functions/classes.

Now there are 3 ways of writing templated code that uses this Incrementable concept we just defined

// Write it like this
template <typename T> 
auto Foo(T t) requires Incrementable<T> {
    return ++t;
}

// Or like this
template <Incrementable T>
auto Foo(T t) {
    return ++t;
}

// Or maybe like this
template <typename T> requires Incrementable<T>
auto Foo(T t) {
    return ++t;
}

// All 3 of them are perfectly valid

At compile time, all calls to Foo() will be checked to see if the T t passed satisfies the Incrementable concept or not

Foo(3) // T is int, compiles fine
Foo(10.34) // T is a float, compiles fine

std::string a = "apples";
Foo(a) // T is a string, ERROR - will fail to compile

The main error message would be

main.cpp:18:20: error: use of function β€˜auto Foo(T) requires Incrementable<T> [with T = std::__cxx11::basic_string<char>]’ with unsatisfied constraints

And this makes sense because std::string does not have overloads for the prefix and postfix increment operators, thus it cannot satisfy our "Incrementable" concept. We catch all these errors at compile time which is always better than crashing programs at run-time.

This is the basic outline of how you could work up a very simple concept and apply it to your generic code.

Concepts in the C++20 Standard Library

You don't always have to write up your own concepts from scratch all the time. The C++20 standard library does provide a lot of basic concepts. You can also use multiple concepts provided in the library and combine them to create your own concepts.

These are some of the concepts provided in the library, all of them are defined in the <concepts> header file

  • std::same_as - specifies that a type is the same as another type

  • std::derived_from - specifies that a type is derived from another type

  • std::convertible_to - specifies that a type is implicitly convertible to another type

  • std::common_reference_with - specifies that two types share a common reference type

  • std::common_with - specifies that two types share a common type

  • std::integral - specifies that a type is an integral type

  • std::signed_integral - specifies that a type is an integral type that is signed

  • std::unsigned_integral - specifies that a type is an integral type that is unsigned

  • std::floating_point - specifies that a type is a floating-point type

  • std::assignable_from - specifies that a type is assignable from another type

  • std::destructible - specifies that an object of the type can be destroyed

  • std::constructible_from - specifies that a variable of the type can be constructed from or bound to a set of argument types

  • std::default_initializable - specifies that an object of a type can be default constructed

  • std::move_constructible - specifies that an object of a type can be move constructed

  • std::copy_constructible - specifies that an object of a type can be copy constructed and move constructed

You can view a list of all the concepts given in the standard library here

Some examples using the standard library concepts

template <typename T>
auto add(T x) requires std::integral<T> { // T can only be int
    return x+10;
}
template <typename T>
auto foo(T t) requires std::destructible<T> { 
    // T needs to have a destructor
    t.~T();
}

you can also apply multiple concepts using && and ||

template <typename T>
auto Foo(T t) requires std::default_initializable<T> && std::destructible<T> {
    // T needs to have a default constructor and a destructor
    T temp;
    t.~T();
}

or

template <typename T>
concept is_numeric = requires {std::integral<T> || std::floating_point<T>;};

Declaring Custom Concepts

There are various ways to mix and match the library concepts along with our custom code. For a type to be valid for a particular concept, it has to meet all the constraint requirement statements.

template <typename T>
concept AddableAndFloat = requires(T x, T y) {
    // x and y can be added
    // x+y does not throw any exceptions
    // and the sum can be converted to a float
    {x+y} noexcept -> std::convertible_to<float>;
};

template <typename T>
float add(T a, T b) requires AddableAndFloat<T> {
    return (float)a+b;
}

add(10, 2); // OK
add(1.223, 76.32); // OK
add("a", "b"); // compilation error

you can also do something like this,

template <typename T>
concept is_numeric = requires (T a, T b) {
    std::destructible<T>;
    std::convertible_to<T, float>;
    {(float)a + (float)b} noexcept -> std::convertible_to<float>;
    a++;
    ++a;
};

for the requirements

  • T must have a constructor
  • T can be casted to a float
  • and objects of T can be incremented

You can have a look at some more complicated examples at :

In Conclusion

Using concepts gives developers the ability to declare abstractions in a much more precise way. It reduces ambiguity and brings clarity to the interfaces we create in code.

The major benefit is that now we have features that bring compile time checking to the template parameters. A huge number of errors coming up due to invalid template parameters, that were only possible to catch by various testing methods can now be fixed right at the compile time and it is awesome.

Using concepts also dramatically increases code readability, without concepts, the template requirements were hidden either in the implementation or in the documentation. Now they are right in front of you without any cost on run time performance.

It is usually advised to use concepts for all template arguments and use combinations of the concepts from the standard library to reduce the surface area for edge cases and errors.

As a bonus tip for making it to the end of the article, here is a video of Bjarne Stroustrup talking about concepts on the Lex Friedman podcast.

C++20 is really exciting. I hope you enjoyed the article. Thanks for reading.

Follow me on twitter or have a look at my github

No Comments Yet