dsp::ProcessorChain and C++ 20 concepts – why does it not work?

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 :wink: 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.

You can maybe a do manual requires
https://en.cppreference.com/w/cpp/language/constraints
with a constrexp and if

template<class T>
std::string optionalToString(T* obj)
{
    constexpr bool has_toString = requires(const T& t) {
        t.toString();
    };

    if constexpr (has_toString)
        return obj->toString();
    else
        return "toString not defined";
}

By “manual requires” you mean assigning the result of a function local requires statement to a constexpr bool as shown in the code example snippet? That does not make any difference in my case.

By the way, my use-case is actually more like this:

template <class Processor, class SampleType>
struct ProcessorWrapper : public Processor
{
    void process (const juce::dsp::ProcessContextReplacing<SampleType>& ctxt) 
    requires supportsReplacingProcessing<Processor, SampleType>
    {
        // some pre processing steps
        Processor::process (ctxt);
        // some post processing steps
    }

    void process (const juce::dsp::ProcessContextNonReplacing<SampleType>& ctxt) 
    requires supportsNonReplacingProcessing<Processor, SampleType>
    {
        // some pre processing steps
        Processor::process (ctxt);
        // some post processing steps
    }
}

so it’s about conditionally enabling/disabling member functions. But the requires statement seems to fail no matter in which context it is used and I found the example above to be the most explicit showcase for the issue.

no I meant to check this in a for before declaring the ProcessorChain template
using a variadic template and a constexp with requires on each type