Exotic CRTP: Enforcing Strict Interfaces Without Friends Using C++23 Explicit Object Parameters

I’ve been experimenting with CRTP and ended up with a variation that enforces a strict interface/implementation boundary without friend declarations. The goal was to eliminate boilerplate I frequently encountered when trying to encapsulate derived class methods.

The key idea is using C++23 explicit object parameters this + a small access wrapper type so implementations can only be called through the interface layer.

That was about two and a half months ago. Since, I’ve taken the time to better understand it and write an article about it, which you can find below. As explained there, I refer to this approach as Exotic CRTP.


Example

// Reference example of the pattern
// See: https://medium.com/@felixolivierdumas/exotic-crtp-rethinking-static-polymorphism-with-c-23-89f9e75e8ffd

#include <iostream>
#include <type_traits>
#include <utility>

namespace exotic {

template<typename... From>
struct crtp_access : From... {};

template<typename T>
constexpr decltype(auto) as_crtp(T&& obj) noexcept {
    using crtp_access_t = crtp_access<std::remove_cvref_t<T>>;
    return static_cast<crtp_access_t&&>(obj);
}

}

struct Base {
    void interface(this auto&& self) {
        exotic::as_crtp(self).implementation();
    }
};

struct Derived : Base {
    void implementation(this exotic::crtp_access<Derived> self) {
        std::cout << "Derived implementation" << std::endl;
    }
};

int main() {
    Derived d;

    d.interface(); // perfectly works

    // d.implementation(); -> doesn't work, Derived only allows .interface()
}

Not sure yet if this is actually useful in real conditions or just a different way of structuring CRTP, but it seems to be genuinely powerful.

Full write-up here: https://medium.com/@felixolivierdumas/exotic-crtp-rethinking-static-polymorphism-with-c-23-89f9e75e8ffd

Curious how this compares to traditional CRTP + friend patterns in real codebases :)

medium.com
u/Mysticatly — 8 days ago
▲ 28 r/Cplusplus+2 crossposts

Exotic CRTP: Enforcing Strict Interfaces Without Friends Using C++23 Explicit Object Parameters

I’ve been experimenting with CRTP and ended up with a variation that enforces a strict interface/implementation boundary without friend declarations. The goal was to eliminate boilerplate I frequently encountered when trying to encapsulate derived class methods.

The key idea is using C++23 explicit object parameters this + a small access wrapper type so implementations can only be called through the interface layer.

That was about two and a half months ago. Since, I’ve taken the time to better understand it and write an article about it, which you can find below. As explained there, I refer to this approach as Exotic CRTP.


Example

// Reference example of the pattern  
// See: https://medium.com/@felixolivierdumas/exotic-crtp-rethinking-static-polymorphism-with-c-23-89f9e75e8ffd

#include <iostream>  
#include <type_traits>  
#include <utility>

namespace exotic {

template<typename From>  
struct crtp_access : From {};

template<typename T>  
constexpr decltype(auto) as_crtp(T&& obj) noexcept {  
    using crtp_access_t = crtp_access<std::remove_cvref_t<T>>;  
    return static_cast<crtp_access_t&&>(obj);  
}

}

struct Base {  
    void interface(this auto&& self) {  
        exotic::as_crtp(self).implementation();  
    }  
};

struct Derived : Base {  
    void implementation(this exotic::crtp_access<Derived> self) {  
        std::cout << "Derived implementation" << std::endl;  
    }  
};

int main() {  
    Derived d;

    d.interface(); // perfectly works

    // d.implementation(); -> doesn't work, Derived only allows .interface()  
}  

<details> <summary>Show original explanation</summary>

As many comments have mentioned, I'd like to clarify a few details regarding how the cast works.

Let's get straight to the point; the design is neither safe nor unsafe. Let me explain.

First of all, you need to know that the layout of structs/classes in C++ works as follows: in most ABIs, the Base Subobject of a Derived class (either a vtable pointer if polymorphic, or the complete object otherwise) is placed at the Derived's first address. Subsequently, the Derived's data (object) is placed there. This allows for down/upcasting, for example, because the compiler can simply cut the Derived portion to obtain the base, and vice versa.

This layout is not guaranteed by the standard. As I explained, it works with the vast majority of compilers, but there's no absolute certainty that this is how it’s going to appear. I must also reiterate that what I'm presenting today is closer to experimentation and a proof of concept than a finished product. It's an interesting concept; now all that remains is to develop it further.

So why am I explaining this? Because it's precisely with this mechanism that I can explain what happens during the cast to crtp_access<T>. Indeed, if we look closely at crtp_access<T>, we can see that it's empty. Therefore, if it inherits from any database (non-virtual; the design doesn't work if there's virtual inheritance in the chain), we can agree that its size will be equal to sizeof(T) + sizeof(crtp_access<T>), which is 0. This means that in memory, crtp_access<T> is exactly the same size as T. In addition to being the same size as T, in memory it is literally identical to it.

So, when we cast from T to crtp_access<T>, we are indeed performing an 'unsafe' cast, but it's still OK because it's as if we were casting from T to T. It's hacky, I admit, but I like to have fun and test things out.

So, design-wise, I agree that it's very hacky. However, I stand by my point that it's not unsafe ONLY in this specific case.

Also, thank you for all your comments. I've taken a lot of advice and it's helped me better understand my own design. I still have a lot to learn and I'm working on it every day. It's moments like these, when I spend four hours reanalyzing my pattern, that push me to improve even more!

</details>

UPDATE: I’ve reworked a big portion of the article to respond to the technical questions and feedback from here. It’s a pretty long read, but I’ve put a lot of effort into it, and I think it’s worth it if you’re interested in the topic.


Here’s the link to the article, it’s a long read (about 5,000 words, ~20 minutes), but I think it’s worth it if you’re into the topic: https://medium.com/@felixolivierdumas/exotic-crtp-rethinking-static-polymorphism-with-c-23-89f9e75e8ffd

Also, here’s a GitHub repo for those who would like to suggest improvements or modifications: https://github.com/unrays/exotic-crtp

reddit.com
u/Mysticatly — 8 days ago