Let's say I am relying on a class that has a private constant member.
class FirstOrderFilter {
public:
FirstOrderFilter(double tau);
double Step(double in);
void Reset(double in = 0);
private:
double in_prev;
double out_prev;
const double tau;
};
I'd like to use an instance of that class, but when I need to reset it, I'd like to totally replace the instance of that object:
FirstOrderFilter filter(1.0);
// do stuff
filter = FirstOrderFilter(0.5);
I think that's a reasonable thing to do. It ensures that you completely replace the object, avoiding any internal states that could otherwise persist. It's similar to preferring case 1 over case 2 in this code:
std::string str;
// do stuff
str = std::string(); // case 1: replace the object, versus
str.erase(); // case 2: rely on the object to reset itself
But when I compile that, the class' private implementation gets in the way, producing:
main.cpp: In function ‘int main()’:
main.cpp:17:34: error: use of deleted function ‘FirstOrderFilter& FirstOrderFilter::operator=(FirstOrderFilter&&)’
17 | filter = FirstOrderFilter(0.5);
| ^
main.cpp:1:7: note: ‘FirstOrderFilter& FirstOrderFilter::operator=(FirstOrderFilter&&)’ is implicitly deleted because the default definition would be ill-formed:
1 | class FirstOrderFilter {
| ^~~~~~~~~~~~~~~~
main.cpp: At global scope:
main.cpp:1:7: error: non-static const member ‘const double FirstOrderFilter::tau’, cannot use default assignment operator
main.cpp: In function ‘int main()’:
main.cpp:17:34: note: use ‘-fdiagnostics-all-candidates’ to display considered candidates
17 | filter = FirstOrderFilter(0.5);
| ^
That's clear. Because of its private const
member, it can't generate an operator=
that doesn't violate const
rules. But a user shouldn't really care about that implementation.
An obvious work-around is using pointers instead of direct objects:
FirstOrderFilter* filter = new FirstOrderFilter(1.0);
// ...
delete filter;
filter = new FirstOrderFilter(0.5);
or
auto filter = std::make_unique<FirstOrderFilter>(1.0);
// ...
filter = std::make_unique<FirstOrderFilter>(0.5);
But this has two disadvantages:
free
and malloc
and to create the new object (slower)filter
because it's now a pointer.A better solution could be the use of placement new
to avoid memory allocation completely.
FirstOrderFilter filter(1.0);
//...
filter.~FirstOrderFilter();
new (&filter) FirstOrderFilter(0.5);
But I'm told this is highly unusual, as placement new is mostly for implementing your own memory allocation (ring buffers, shared memory, etc), and not for normal everyday use for things like object replacements.
Is this a reasonable usage of placement new? Or is there a more obvious solution that I'm missing (perhaps a const_cast
or mutable
something-or-other).
The practical problem is that
FirstOrderFilter foo{4.2};
bar(foo);
std::cout << foo.getTau();
is allowed to assume foo.tau
is still 4.2
after the call to bar
, because it's a const
member.
That's what const
members mean, and permitting that optimization is probably the only reason to use them.
If bar
might have magically mutated that const
member where your code can't see it (whether by placement new or any other mechanism), you have an entirely predictable problem.
To do this safely, you need to somehow communicate to the caller that foo.tau
might have changed.
The std::launder
option isn't great: bar
would need to return a pointer to the same object and the caller would have to only refer to the object via the pointer afterwards. This is obviously error-prone and unwieldy.
You can see P0532 On Launder (PDF) for more discussion of the problem and why std::launder
doesn't really solve it.
Another possibility is to wrap the object in a union, or variant
, or optional
or something to make it clear that it could be replaced by a different instance when you're not looking - but you need to pass the wrapper everywhere, and again it's unwieldy.
A much better solution is to not use const
for things you actually want to change sometimes.
PS. there's a related optimization, devirtualization, that has exactly the same problem (if you replace a dynamically-typed object with one of another type). This makes intuitive sense if you think of a vtable pointer (or equivalent RTTI implementation detail) as a const
member. Anyway, don't do that either.