Concepts and CRTP

I’ve recently updated some C++ libraries which make use of CRTP at my workplace to C++20. Since C++20 has brought us the concepts language feature I thought this would be a great chance to apply it and type-check the derived class which the librarie’s CRTP base gets downcasted to. Let’s take a look at the initial situation.

#include <cstdio>

template<typename T>
struct CrtpBase {
  void bar() {
    puts(__PRETTY_FUNCTION__);
    static_cast<T&>(*this).foo();
  }
};

struct Derived : CrtpBase<Derived> {
  void foo() { puts(__PRETTY_FUNCTION__); }
};

int main(int argc, char* argv[]) {
  Derived d;
  d.bar();
}


We’ve got the base called CrtpBase which relies on the implementation Derived to provide a foo method. Adding a concept to verify that Derived provides foo is pretty straight forward.

template<typename T>
concept Fooable = requires(T t) {
  {t.foo()};
};

template<Fooable T>  // Replace generic typename with concept
struct CrtpBase {
  void bar() {
    puts(__PRETTY_FUNCTION__);
    static_cast<T&>(*this).foo();
  }
};

But now all of a sudden our code stops to compile. Why? Because the type Derived is incomplete at the time the constraints of our concept are checked. As a workaround we can delay the concept check to some point later on when Derived is fully known to the compiler. The most elegant solution I’ve found so far is to constraint the return type of a method which performs the static_cast for us.

template<typename T>
struct CrtpBase {
  void bar() {
    puts(__PRETTY_FUNCTION__);
    impl().foo();
  }

private:
  Fooable auto& impl() { return static_cast<T&>(*this); }
};

I’d still recommend to be very cautious when using concepts whenever it’s output depends on the time of instantiation. Consider the following snippet which I fondly dubbed the Schrödinger concept.

#include <cstdio>

template <typename T>
concept Fooable = requires(T t) {
  {t.foo()};
};

template <typename T>
struct A {
  void bar() { printf("Fooable<T> check in A %d\n", Fooable<T>); }
};

template <typename T>
struct B {
  static constexpr auto is_fooable{Fooable<T>};
  void bar() { printf("Fooable<T> check in B %d\n", Fooable<T>); }
};

struct SA : A<SA> {
  void foo() {}
};

struct SB : B<SB> {
  void foo() {}
};

int main() {
  SA sa;
  sa.bar();
  SB sb;
  sb.bar();
}

Running this snippet with Clang 11.0.0 produces the following output

Fooable<T> check in A 1 Fooable<T> check in B 0

while GCC 10.2.0 produces

Fooable<T> check in A 1 Fooable<T> check in B 1

I’m not even sure if either compiler is correct and my best guess is that instantiation order is simply undefined…?