Wednesday, May 5, 2021

C++20 Concepts

C++20 Concepts


This week (2021) I'm at the C++Now conference. The conference is normally in Aspen, Colorado (I was there more than 10 years ago), but this year it's online, for obvious reasons.

Yesterday there were two excellent interesting talks from Jeff Garland about concepts in C++20, how it works, what you can do with it, how you can use them and how you can write them.

Some compilers (notably clang) don't yet support concepts, but you can already play with toy projects online using https://godbolt.org, where your code is compiled and run on the fly.

So here is my toy project, which I wrote during the talks (there were two consecutive talks):

 

#include <array>
#include <type_traits>
#include <iostream>

template <typename C> concept ValidCoordinateType = std::is_arithmetic_v<C>;
template <int D> concept ValidDimension = D >= 2 and D <= 3;
template <int D> concept HasZ = D >= 3;

template <typename C, int D>
requires(ValidDimension<D> and ValidCoordinateType<C>)
struct mypoint
{
  mypoint() = default;
 
  mypoint(C x, C y) 
    : coors{x, y} {}
 
  // Only available for 3D
  mypoint(C x, C y, C z) requires HasZ<D>
    : coors{x, y, z} {}
 
  auto x() const { return coors[0]; }
  auto y() const { return coors[1]; }

  // Avoids compilation for Dim < 3
  auto z() const requires HasZ<D> { return coors[2]; }
 
private :
   std::array<C, D> coors;
};

struct mytype {};

int main()
{
  mypoint<double, 2> two(1, 2);
  mypoint<float, 3> three(3, 4, 5);
  std::cout << "Hi " << two.x() << " " << two.y() << " " << three.z() << "\n";

  // These declarations will not compile
  //mypoint<mytype, 2> p1;
  //mypoint<double, 4> p2;
 
  // This line will not compile
  //std::cout << two.z(); // Fails because .z() is not available

  return 0;
}

So this code is using C++20 concepts. You can see it clearly at and around the keywords concept and requires. It is really cool, especially compared with what we had to do with C++03 (Boost.Geometry was written in C++03 and only a few releases ago went to C++14) to achieve this.

Some things could earlier be done with static_assert too (such as checking the number of dimensions), but now the compiler neatly warns that that is better written using a concept.

Other things needed SFINAE earlier (such as the enabling  / disabling the functions for the Z coordinate).

If the last declarations in main are uncommented, you get a neat compiler error report (for instance for p2):

<source>: In function 'int main()':
<source>:40:20: error: template constraint failure for 'template<class C, int D> requires (ValidDimension<D>) && (ValidCoordinateType<C>) struct mypoint'
40 | mypoint<double, 4> p2; // Fails because 4 coordinates are not allowed by the concept
| ^
<source>:40:20: note: constraints not satisfied
<source>:6:26: required for the satisfaction of 'ValidDimension<D>' [with D = 4]
<source>:6:56: note: the expression 'D <= 3 [with D = 4]' evaluated to 'false'
6 | template <int D> concept ValidDimension = D >= 2 and D <= 3;
| ~~^~~~

Wow, it's only a few lines! And so clear! In the past we got hundreds of hard to interpret error messages about templates...

And if you uncomment the line streaming z for a 2d coordinate, you get:

<source>: In function 'int main()':
<source>:43:22: error: no matching function for call to 'mypoint<double, 2>::z()'
43 | std::cout << two.z();
| ^
<source>:23:8: note: candidate: 'auto mypoint<C, D>::z() const requires HasZ<D> [with C = double; int D = 2]'
23 | auto z() const requires HasZ<D>
| ^
<source>:23:8: note: constraints not satisfied
<source>: In instantiation of 'auto mypoint<C, D>::z() const requires HasZ<D> [with C = double; int D = 2]':
<source>:43:22: required from here
<source>:7:26: required for the satisfaction of 'HasZ<D>' [with D = 2]
<source>:7:35: note: the expression 'D >= 3 [with D = 2]' evaluated to 'false'
7 | template <int D> concept HasZ = D >= 3;
| ~~^~~~