c++static-castclass-templatestatic-polymorphism

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?


Solution

  • 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.