Search code examples
c++placement-new

Is this an abuse of placement new?


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:

  1. It calls free and malloc and to create the new object (slower)
  2. I need to change syntax in all of my other uses of 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).


Solution

  • 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.