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... { };
This construction is fairly fundamental in the way I pass… You guessed it, interfaces, to each module in a processing chain. 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); // call Reader::read(16)
interface.write(buf);
}
Okay. A bit much to take in here, but the gist of it, is that both read
from the Reader
and write
from the Writer
are both 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.
void shift_right(auto interface) {
auto buffer = interface.read(16);
for (auto& sample : buffer) {
sample >>= 1;
}
interface.write(buf);
}
Caveats
This approach is not without caveats, and before you go ahead an implement something like this, do note the following:
Members with the same name on the inherited classes 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 pretty elaborate documentation to be useful for an end user. That might be easier if you use some C++20 concepts to instantiate and eliminate different functions from the Interface
, however that would require all member function implementations to be within the Interface. With that being said, one could conceive an Interface
concept that would allow for all of that.
Conclusion
This is a pretty elaborate way to merge two types together, but 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.
Thanks for reading