c++templatestype-traitscrtp

Static assert on interface of type defined using CRTP


I'm working on a library where client code can define certain types using CRTP. I've extracted the problem into the following code:

template <typename T, typename X>
struct A
{   
    void test(X x)
    {
        return static_cast<T &>(*this).test(std::forward<X>(x));
    }
};

struct B: public A<B, int>
{
    void test(int x){};
};

int main(void)
{
    B b;
    b.test(5);
};

I want to have the compiler validate that client-defined types (struct B in the example code) provide the required interface. The above code with segfault if test is not defined on B. I wrote a type trait, api_defines_test<T> that can check if the test member function is defined. I realize now that this won't work: the trait is satisfied by the test member function in the base class, and this approach also requires me to know the concrete type of the user-defined classes which obviously isn't possible. I'm not asking how I can assert on types that are not yet defined, but is there some clever way to get a similar effect?


Solution

  • You can place a static_assert in the A::test method where you have access to T (which is the user defined CRTP type). You can apply whatever type constraints you need at that point even though the CRTP type has not been defined yet -- that is a feature of CRTP.

    The methods in A should have different names than their corresponding implementations in B as mentioned by @vvv444 in the comments. In the canonical CRTP pattern, A provides the interface and B the implementation.

    Sample Code

    #include <iostream>
    
    using std::cout, std::endl;
    
    template <typename T, typename X>
    struct A {
        void test(X x) {
            static_assert(std::is_member_function_pointer_v<decltype(&T::test_impl)>);
            return static_cast<T &>(*this).test_impl(std::forward<X>(x));
        }
    };
    
    struct B: public A<B, int> {
        void test_impl(int x){};
    };
    
    int main(void) {
        B b;
        b.test(5);
    };
    

    If you do not declare B::test_impl, the static_assert will trigger. Of course, in this case the assert is redundant because you will get a compilation error without the assert. This just demonstrates the ability to apply type constraints to the CRTP type in A.

    Output

    src/p1.cpp:11:70: error: no member named
          'test_impl' in 'B'
            static_assert(std::is_member_function_pointer_v<decltype(&T::test_impl)>);
                                                                      ~~~^
    src/p1.cpp:22:7: note: in instantiation of
          member function 'A<B, int>::test' requested here
        b.test(5);