I’ve made a bit progress that might be interesting:
First of all using the Pimpl idiom for “meta” classes helps. I lose the ability of the compiler optimizing the chains for these use cases (plus there’s a minor performance penalty for heap access), but it’s a viable tradeoff via exponential code generation.
// header:
class wrapped_meta:
{
wrapped_meta();
~wrapped_meta();
void process(ProcessData& d);
void* pimpl;
}
// Source.cpp
using MyType = container::chain<gain, compressor, container::split<whatever, delay>>;
wrapped_meta()
{
pimpl = new MyType();
}
~wrapped_meta()
{
delete reinterpret_cast<MyType*>(pimpl); // Ah, the good old raw delete...
}
wrapped_meta::process(ProcessData& d)
{
reinterpret_cast<type*>(pimpl)->process(d);
}
// Usage in other file
// this signal chain is not able to optimize it on compile time due to the opaque pimpl pointer,
// but rather treats it as encapsulated entity.
using MyChainUsingMeta = container::chain<gain, wrapped_meta, phaser>
Now the most obvious flaw of this approach is the excruciating ugliness of the void*
pointer and the usage of reinterpret_cast
and raw new
and delete
calls. However the alternative would be a forward declaration of MyType
in the header file, which would lead to the template instantiation in the header so we would be back at square one. Am I missing another option here?
The other thing I noticed is that there’s a significant reduction of the object file size if I choose another implementation technique for the variadic template containers:
// JUCE way (heavily simplified)
template <class Processor, class SubType>
class element
{
void process(ProcessData& d)
{
obj.process(d);
}
Processor obj;
};
template <class First, class... Others>
class chain: public element<First, chain<Others...>
{
using Base = element<First>;
void process(ProcessData& d)
{
Base::process(d);
others.process(d);
}
chain<Others...> others;
};
// Alternative implementation
template <class... Processors>
class AlternativeChain
{
std::tuple<Processors...> processors;
void process(ProcessData& d)
{
for_each(processors, [d](auto& p){ p.process(d); });
}
}
the for_each
is also heavily simplified and includes compile-time iteration over the std::tuple
using a std::index_sequence
- also the lambda is a member function.
Using the alternative implementation reduces the object file size about 50% (plus the implementation gets much easier to understand). Now my variadic template skills hardly extend beyond copy & pasting stuff from Stack overflow, but the logical explanation is that it does not have to generate classes for all the base classes of chain
:
chain<gain, amp, reverb, chorus>;
=> creates classes:
class c1; // chain<chorus>
class c2; // chain<reverb, c1>
class c3; // chain<amp, c2>
class c4; // chain<gain, c3>
Instead, it creates one class containing a std::tuple
and then just one process
method implementation per tuple element. Assuming this is correct, is there a reason why the juce::dsp::ProcessorChain class used the other approach?