I’ve run into this issue as well. As per the c++ core guidelines, in our codebase I’m trying to stick to passing const references to objects by default, in order that interfaces are explicit about when objects should and shouldn’t be modified. e.g.:
// Cannot reassign block, nor modify the audio data it points to
void a(const AudioBlock<const float>& block);
// Cannot reassign block, but may modify the audio data it points to
void b(const AudioBlock<float>& block);
// Can reassign block, but the audio data it points to is immutable
void c(AudioBlock<const float>& block);
// Can reassign block, and the audio data it points to is mutable
void d(AudioBlock<float>& block);
Unfortunately we often see interfaces which look like c or d, when what we actually want to enforce is a or b. This permits the following kind of error which could otherwise be prevented by the compiler:
void pathological(AudioBlock<float>& block)
{
// I just love creating audio glitches, mwahahaha!
block = block.getSubBlock(0, block.getNumSamples() / 2);
}
void myProcessFunction(AudioBlock<float>& block)
{
pathological(block);
// Was I expecting that function call to have altered the
// length of block? Was the caller? Probably not!
}
Frustratingly, the fact that ProcessContextReplacing takes a non-const AudioBlock& forces us either to use less safe interfaces of the form c and d, or do some hacky const_cast’ing to work around this.
IMHO I agree with others here in that the better thing would be for ProcessContextReplacing to take and return a const AudioBlock& for the output block. This would explicitly prevent nonsense like this:
void pathological2(ProcessContextReplacing<float> context)
{
context.getOutputBlock() = someOtherAudioBlock;
}
void myFunction()
{
AudioBuffer<float> myBuffer{ 2, 64 };
AudioBlock<float> myBlock( myBuffer };
pathological2( { myBlock } );
// Oh dear, myBlock may not point at myBuffer anymore!
}
Using a const& would allow users to pass temporaries to the constructor. ProcessContextNonReplacing holds a reference to the outputBlock, which will dangle if the user passes a temporary.
If I’m not mistaken, this could also be solved by declaring a deleted rvalue reference constructor:
ProcessContextReplacing(const AudioBlock<T>&);
ProcessContextReplacing(AudioBlock<T>&&) = delete; // prevent construction from a temporary, which would result in a dangling reference
I do appreciate the point about rippling/breaking changes though.