Reputation: 25265
My first foray in to C++ is building an audio synthesis library (EZPlug).
The library is designed to make it easy to set up a graph of interconnected audio generator and processor objects. We can call the EZPlugGenerators
All of the processor units can accept one or more EZPlugGenerators as inputs.
Its important to me that all configuration methods on these EZPlugGenerators are chainable. In other words, the methods used in setting up the synthesis graph should always return a pointer to the parent object. That allows me to use a syntax which very nicely shows the nested nature of the object relationships like this:
mixer.addGenerator(
a(new Panner())
->setVolume(0.1)
->setSource(
a(new TriggererPeriodic())
->setFrequency(
v(new FixedValue(1), "envTriggerFreq")
)
->setTriggerable(
a(new Enveloper())
->setAllTimes(v(0.0001), v(0.05), v(0.0f, "envSustain"), v(0.01))
->setAudioSource(
a(new SineWaveMod())
->setFrequency(
a(new Adder())
->addGenerator(a(new Adder()))
->addGenerator(v(5000, "sineFreq"))
->addGenerator(
a(new Multiplier())
->addVal(v("sineFreq"))
->addVal(
a(new TriggererPeriodic())
->setFrequency(v("envTriggerFreq"))
->setTriggerable(
a(new Enveloper())
->setAllTimes(0.1, 0.1, 0, 0.0001)
->setAudioSource(v(1, "envAmount"))
)
)
)
)
)
)
)
);
The "a" and "v" functions in the above code store and return references to objects and handle retrieving them and destroying them.
I suspect my approach to C++ looks a little weird, but I'm finding that the language can actually accommodate the way I want to program fairly well.
Now to my question
I'd like to create a common superclass for all EZPlugGenerators which can accept inputs to inherit from. This superclass would have a method, "addInput", which would be overridden by each subclass. The problem comes from the fact that I want "addInput" to return a pointer to an instance of the subclass, not the superclass.
This isn't acceptable:
EZPlugProcessor* addInput(EZPlugGenerator* generator)
because that returns a pointer to an instance of the superclass, not the sublass destroying the chainability that I'm so happy with.
I tried this:
template<typename T> virtual T* addInput(EZPlugGenerator* obj){
but the compiler tells me I can't create a virtual template function.
I don't HAVE to use inheritance here. I can implement 'addInput' on every single EZPlugGenerator that can take an input. It just seems like gathering all of them under a single parent class will help make it clear that they all have something in common, and will help enforce the fact that addInput
is the proper way to plug one object in to another.
So, is there a way I can use inheritance to dictate that every member of a group of classes must implement an 'addInput' method, while allowing that method to return a pointer to an instance of the child class?
Upvotes: 2
Views: 189
Reputation: 279245
Virtual functions in C++ can have covariant return types, which means that you can define
virtual EZPlugProcessor *addInput(EZPlugGenerator* generator) = 0;
in the base class, and then
struct MyProcessor : EZPlugProcessor {
virtual MyProcessor *addinput(EZPlugGenerator* generator) {
...
return this;
}
};
As long as the caller knows (by the type they're using) that the object is a MyProcessor
, they can chain addInput
together with other functions specific to MyProcessor
.
If your inheritance hierarchy has more levels, then unfortunately you'll sometimes find yourself writing:
struct MySpecificProcessor : MyProcessor {
virtual MySpecificProcessor *addinput(EZPlugGenerator* generator) {
return static_cast<MySpecificProcessor*>(MyProcessor::addInput(generator));
}
};
because there's no way to specify in EZPlugProcessor
that the return type of addInput
is "pointer to the most-derived type of the object". Each derived class has to "activate" the covariance for itself.
Upvotes: 4
Reputation: 96241
Yes, C++ already provides for covariant return types.
class Base
{
public:
virtual Base* add() = 0 { return <some base ptr>; }
};
class Child : public Base
{
public:
virtual Child* add() { return <some child ptr>; }
};
On the other hand no one will ever be able to read your code so you might want to consider if there's an alternate way to set up the configuration than writing LISP chaining in C++.
Upvotes: 1