We are using a lot of C++ 20 concepts in our code lately. Among other use-cases, constraining juce dsp module style processors is one of the most interesting use cases for us.
Now we have built some processor related concepts, such as
template <class Processor, class SampleType>
concept supportsNonReplacingProcessing = requires (Processor& p, juce::dsp::ProcessContextNonReplacing<SampleType> c) { p.process (c); }
which should constrain a processor type to be compatible to juce::dsp::ProcessContextNonReplacing
. Now let’s look at this example:
struct OnlyReplacingProcessor
{
void prepare (const juce::dsp::ProcessSpec&) {};
void process (const juce::dsp::ProcessContextReplacing<float>&) {}
void reset() {}
};
template <supportsNonReplacingProcessing<float> Processor>
void doSomeProcessing (Processor& p, juce::dsp::ProcessContextNonReplacing<SampleType> c)
{
p.process (c);
}
void shouldFailToCompile()
{
OnlyReplacingProcessor p;
juce::dsp::AudioBlock<float> b;
juce::dsp::ProcessContextNonReplacing<float> c (b, b);
doSomeProcessing (p, c);
}
Compiling this “successfully” gives me a compiler error like
error: no matching function for call to 'doSomeProcessing'
doSomeProcessing (c, ct);
note: candidate template ignored: constraints not satisfied [with P = OnlyReplacingProcessor]
void doSomeProcessing (P& p, juce::dsp::ProcessContextNonReplacing<float> c)
^
note: because 'p.process(c)' would be invalid: no viable conversion from 'juce::dsp::ProcessContextNonReplacing<float>' to 'const juce::dsp::ProcessContextReplacing<float>'
requires requires (P& p, juce::dsp::ProcessContextNonReplacing<float> c) { p.process (c); }
Now if I change the code to
void shouldAlsoFailToCompile()
{
juce::dsp::ProcessorChain<OnlyReplacingProcessor, OnlyReplacingProcessor> p;
juce::dsp::AudioBlock<float> b;
juce::dsp::ProcessContextNonReplacing<float> c (b, b);
doSomeProcessing (p, c);
}
the compiler does not stop with a constraints not satisfied error message but with a pre-concepts cryptic build error when it fails to convert the ProcessContextNonReplacing
into a ProcessContextReplacing
somewhere down in the ProcessorChain
implementation.
Now, a cryptic error message is inconvenient but we were used to this in a pre C++ 20 era for quite a long time The problem here is a different one: If I try to use the
supportsNonReplacingProcessing
concept to conditionally enable or disable code paths depending on the capabilities of a templated processor type it will take the wrong code path if a ProcessorChain
is involved and will end up in a build error.
What I don’t understand here is why does this happen? Usually a concept is not fulfilled if the code block in the require statement would fail to compile. Obviously as seen in the example, the code will not compile, but in case of a ProcessorChain
being involved the compiler won’t get that when evaluating the concept. And I have no clue why? Are there some aspects of the implementation of the ProcessorChain
here that could cause this? Am I experiencing a compiler bug? And does anyone from the JUCE team see a way to resolve this? Maybe @reuk knows something here?
I’m building using the Xcode 14.0.1 toolchain on macOS 12.6.