Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Has CRTP no compile time check?

I was trying to implement static polymorphism using the Curiously Recurring Template Pattern, when I noticed that static_cast<>, which usually checks at compile time if a type is actually convertible to another, missed a typo in the base class declaration, allowing the code to downcast the base class to one of its siblings:

#include <iostream>

using namespace std;

template< typename T >
struct CRTP
{
    void do_it( )
    {
        static_cast< T& >( *this ).execute( );
    }
};

struct A : CRTP< A >
{
    void execute( )
    {
        cout << "A" << endl;
    }
};

struct B : CRTP< B >
{
    void execute( )
    {
        cout << "B" << endl;

    }
};

struct C : CRTP< A > // it should be CRTP< C >, but typo mistake
{
    void execute( )
    {
        cout << "C" << endl;
    }
};

int main( )
{
    A a;
    a.do_it( );
    B b;
    b.do_it( );
    C c;
    c.do_it( );
    return 0;
}

Output of the program is:

A
B
A

Why does the cast work with no errors? How can I have a compile time check that could help from this type of errors?

like image 938
nyarlathotep108 Avatar asked Jan 10 '18 16:01

nyarlathotep108


2 Answers

The usual way to solve this in CRTP is to make the base class have a private constructor, and declare the type in the template a friend:

template< typename T >
struct CRTP
{
    void do_it( )
    {
        static_cast< T& >( *this ).execute( );
    }
    friend T;
private:
    CRTP() {};
};

In your example, when you accidentally have C inherit from CRTP<A>, since C is not a friend of CRTP<A>, it can't call its constructor, and since C has to construct all its bases to construct itself, you can never construct a C. The only downside is that this doesn't prevent compilation per se; to get a compiler error you either have to try to actually construct a C, or write a user defined constructor for it. In practice, this is still good enough and this way you don't have to add protective code in every derived as the other solution suggests (which IMHO defeats the whole purpose).

Live example: http://coliru.stacked-crooked.com/a/38f50494a12dbb54.

NB: in my experience, the constructor for CRTP must be "user declared", which means you cannot use =default. Otherwise in a case like this, you can get aggregate initialization, which will not respect private. Again, this might be an issue if you are trying to keep the trivially_constructible trait (which is not a terribly important trait), but usually it shouldn't matter.

like image 144
Nir Friedman Avatar answered Nov 05 '22 18:11

Nir Friedman


Q1 Why does the cast work with no errors?

When none of the sensible things apply ...

From https://timsong-cpp.github.io/cppwp/n3337/expr.static.cast#2:

Otherwise, the result of the cast is undefined.


Q2 How can I have a compile time check that could help from this type of errors?

I was not able to find a method that can be used in CRTP. The best I could think of is to add static_assert in the derived classes.

For example, if you change C to:

struct C : CRTP< A > // it should be CRTP< C >, but typo mistake
{
   static_assert(std::is_base_of<CRTP<C>, C>::value, "");
   void execute( )
   {
      cout << "C" << endl;
   }
};

You will see the error at compile time.

You can simplify that to

struct C : CRTP< A > // it should be CRTP< C >, but typo mistake
{
   using ThisType = C;
   static_assert(std::is_base_of<CRTP<ThisType>, ThisType>::value, "");
   void execute( )
   {
      cout << "C" << endl;
   }
};

Similar code needs to be added in each derived type. It's not elegant but it will work.

PS I wouldn't recommend using the suggested solution. I think it's too much overhead to account for the occasional human error.

like image 29
R Sahu Avatar answered Nov 05 '22 18:11

R Sahu