In my DSP framework I recently had the need for a generic Interface type, that could have a set of member functions based on the underlying storage type. After much pondering, I remembered a brilliant video by Jason Turner, that showcased how to inherit from lambdas which in turn reminded me of the mixin pattern.

What if I could just inherit from a template type

Which indeed you can… Behold, the Interface struct.

template <typename T>
struct Interface : T { };

This way, every public member on T will be available on any instance of Interface. But this example is quite boring, seeing as one could just make a T and have all the same functionality. But this is where it gets really interesting:

template <typename T1, typename T2>
struct Interface : T1, T2 { };

Introducing a second parameter allows Interface to essentially merge two types together

struct Thing {
    void doSomething();
};

struct OtherThing {
    void doSomethingElse();
};

template <typename T1, typename T2>
struct Interface : T1, T2 { };

void foo() {
    auto i = Interface<Thing, OtherThing>{};

    i.doSomething(); // valid
    i.doSomethingElse(); // valid
}

It could even be a variadic template!

template <typename ... Ts>
struct Interface : Ts... { };

To add a bit more context, we can start implementing some more fun types to inherit from. Imagine having a thing producing samples and a thing consuming samples, you want the consumer to get the samples that the producer creates, however, you don’t necessarily want to specify what kind of samples they are until you know what the producer produces and the consumer consumes. We then have something like the following

#include <span>
#include <array>

template <typename Storage>
struct Reader {
    Reader(Storage& storage) : storage_(storage) {  }

    auto read(int count) { 
        return std::span(storage_.begin(), count);
    }

    Storage& storage_;
};

template <typename Storage>
struct Writer {
    Writer(Storage& storage) : storage_(storage) {  }

    void write(auto data) {
        std::copy(data.begin(), data.end(), storage_.begin());
    }

    Storage& storage_;
};

template <typename Storage, typename Reader, typename Writer>
struct Interface : Reader, Writer { 
    Interface()  : Reader(storage),
                   Writer(storage) {}
    Storage storage{};
};

void foo() {
    using Storage = std::array<float, 128>;
    using Reader = Reader<Storage>;
    using Writer = Writer<Storage>;

    auto interface = Interface<Storage, Reader, Writer>();
    auto buf = interface.read(16); // calls Reader::read(16)
    interface.write(buf); // calls Writer::write(buf)
}

A bit much to take in here, but the gist of it is that both read from the Reader struct and write from the Writer struct are exposed on the Interface struct. With C++20 auto function parameters we can then completely erase all of the template nonsense from the implementer and have a class or simply a function take an interface as its parameter.

void shift_right(auto interface) {
    auto buffer = interface.read(16);
    
    for (auto& sample : buffer) {
        sample >>= 1;
    }

    interface.write(buf);
}

Caveats

Do note that if your base classes have members with the same name, those will have to be qualified as it would otherwise be ambiguous. For example

void shift_right(auto interface) {
    // interface.storage_; // ERROR
    interface.Writer::storage_; // valid
    interface.Reader::storage_; // valid
}

There is also no way for your autocomplete backend to help you find out what members the interface does have, so something like this would have to come with some form of documentation to be useful for an end user. With that being said, one could come up with an Interface concept that would make all of this clearer

template <typename T>
concept Interface = requries(T interface){
    interface.read();
    interface.write();
};

Conclusion

The possibilities are pretty much endless. It has become a major part of my DSP framework and I cannot possibly imagine that this is the last time I use this technique.