December 13th, 2024

Converting to a derived class from the future: How to cast from a base class to an incomplete derived class?

A customer had a header-only design in which the clients of the header are expected to derive from the base class with a specific class name. Sort of like CRTP without the CR. Or the even the T.

// awesome.h

// Client is expected to define this class
// as a derived class of AwesomeAppBase.
struct AwesomeApp;

struct AwesomeAppBase
{
    AwesomeApp* derived()
    { return static_cast<AwesomeApp*>(this); }

    // AwesomeApp may override this method to provide
    // custom preparation work.
    void PrepareForSomething() {}

    void DoSomething()
    {
        derived()->PrepareForSomething();
        ⟦ .. do something ... ⟧
    }
};

This doesn’t work because Awesome­App is an incomplete type, so the static_cast<AwesomeApp*>(this) doesn’t know how to convert from an Awesome­App­Base to an Awesome­App.

Normally, you would fix this problem by deferring the definition of the methods like derived() and Do­Something() to a point after Awesome­App has been defined, but this is a header-only library, so there’s no chance to provide code later. The header is your only chance.

We can use a trick we saw earlier: Using a type before it is defined. In this case, we templatize the methods that depend on the definition of Awesome­App by giving them a template type parameter whose default is the incomplete type.

struct AwesomeAppBase
{
    template<typename T = AwesomeApp>
    AwesomeApp* derived()
    { return static_cast<T*>(this); }

    // AwesomeApp may override this method to provide
    // custom preparation work.
    void PrepareForSomething() {}

    template<typename T = AwesomeApp>
    void DoSomething()
    {
        derived<T>()->PrepareForSomething();
        ⟦ .. do something ... ⟧
    }
};

It is, however, rather awkward to have to templatize every method that depends on the Awesome­App. You can remove that awkwardness by templatizing the entire class, transforming it into true CRTP, and then making Awesome­App­Base be an alias for the version of the template with Awesome­App as the derived class.

template<typename T>
struct AwesomeAppBaseT
{
    T* derived()
    { return static_cast<T*>(this); }

    // AwesomeApp may override this method to provide
    // custom preparation work.
    void PrepareForSomething() {}

    void DoSomething()
    {
        derived()->PrepareForSomething();
        ⟦ .. do something ... ⟧
    }
};

using AwesomeAppBase = AwesomeAppBaseT<AwesomeApp>;

You have to be careful to say T instead of Awesome­App inside the Awesome­App­Base­T so that you use the dependent type and therefore defer the name lookup until the point the template is instantiated.¹

To remove the temptation to say Awesome­App prematurely, you can move the forward declaration of Awesome­App to appear after the template. And you can even use Awesome­App as the name of the template type parameter, so the code looks “normal”.

template<typename AwesomeApp>
struct AwesomeAppBaseT
{
    AwesomeApp* derived()
    { return static_cast<AwesomeApp*>(this); }

    // AwesomeApp may override this method to provide
    // custom preparation work.
    void PrepareForSomething() {}

    void DoSomething()
    {
        derived()->PrepareForSomething();
        ⟦ .. do something ... ⟧
    }
};

// Client is expected to define this class
// as a derived class of AwesomeAppBase.
struct AwesomeApp;

using AwesomeAppBase = AwesomeAppBaseT<AwesomeApp>;

If you don’t want anybody to specialize Awesome­App­BaseT with anything other than Awesome­App, you can move it into a details namespace. But it seems useful to let people use the full CRTP form, if they want to give their derived class some other name, or if they want to create more than one derived class. (Say, because they want to have a Contoso­Awesome­App and a Fabrikam­Awesome­App and choose between them at runtime based on a command line switch.)

¹ Note that declaring a type alias does not instantiate the type. According to [temp.inst], instantiation occurs “when the specialization is referenced in a context that requires a completely-defined object type or when the completeness of the class type affects the semantics of the program,” neither of which applies here.

Topics
Code

Author

Raymond has been involved in the evolution of Windows for more than 30 years. In 2003, he began a Web site known as The Old New Thing which has grown in popularity far beyond his wildest imagination, a development which still gives him the heebie-jeebies. The Web site spawned a book, coincidentally also titled The Old New Thing (Addison Wesley 2007). He occasionally appears on the Windows Dev Docs Twitter account to tell stories which convey no useful information.

0 comments