2

[ This is a follow-up to can memcpy() be used to change “const” member data?. And Idiomatic Way to declare C++ Immutable Classes really gets at the issue, especially this answer "In a language designed around immutable data, it would know it can "move" your data despite its (logical) immutability." ]


Given a struct with const members

struct point2d { const int x; const int y; }; // can't change to remove "const"

A class which holds a pointer to point2d can point to a new point2d instance with different values.

struct Bar
{
    std::unique_ptr<point2d> pPt_{ new point2d{ 0, 0 } };
    const point2d& pt() const {
        return *pPt_;
    }

    void move_x(int value) {
        pPt_.reset(new point2d{ pt().x + value, pt().y });
    }
};

Clients of Bar see:

   Bar bar; // (0, 0)
   bar.move_x(3141); // (3141, 0)

Both point2d and Bar are working exactly as desired; yes, point2d is completely immutable.

However, I'd really like a different implementation of Bar that stores the point2d instance as member data. Is there any way to achieve this? Using placement new supposedly results in undefined behavior (see comment).

#include <new>
struct Baz
{
    point2d pt{ 0, 0 };

    void move_x(int value) {
        // ** is this undefined behavior ? **
        new (&pt) point2d { pt.x + value, pt.y };
    }
};

Does not using point2d directly as member data work-around the (potential?) undefined behavior?

struct Blarf
{
    unsigned char pt_[sizeof(point2d)];
    const point2d& pt() const {
        return *reinterpret_cast<const point2d*>(pt_);
    }

    Blarf() {
        new (&pt_) point2d{ 0, 0 };
    }

    void move_x(int value) {
        new (&pt_) point2d{ pt().x + value, pt().y };
    }
};

Which is correct? Just Blarf? Or is Baz also OK? Or neither, and the only solution is Bar?

Community
  • 1
  • 1
Ðаn
  • 10,934
  • 11
  • 59
  • 95
  • Why do you say that placement new results in UB when the "proof" you linked to has nothing to do with placement new? What am I missing? – Lightness Races in Orbit Jul 08 '16 at 15:54
  • 2
    Oh comments, blah. Not sure I agree with Sam though. Placement new as-if destroys the original (which is well-defined if it's trivially destructible, like an `int`) and replaces it with something new. Mind you, an "object" is a region in memory. But it also has a type. The two are at odds for placement new. I think the question as to whether that _would_ be UB would be quite interesting. I'd ask that question first. – Lightness Races in Orbit Jul 08 '16 at 15:58
  • 1
    @Dan: Off the top of my head, as long as you were to fix potential alignment issues with that array, I can't think of a problem with it. – Lightness Races in Orbit Jul 08 '16 at 16:10
  • BTW that's a really silly `point2d` class; my condolences – Lightness Races in Orbit Jul 08 '16 at 16:10
  • 2
    Adding in more confusion - what if you explicitly invoked the destructor for the object, then rebuilt it with placement new, as in: `pt.~point2d(); new (&pt) point2d{x, y};`? That seems like it might actually be conformant, since the lifetime of the original object is explicitly ended and a new one rebuilt in its place... – templatetypedef Jul 08 '16 at 16:19
  • Yeah if your type has non-trivial destruction then that covers you – Lightness Races in Orbit Jul 08 '16 at 16:26
  • @LightnessRacesinOrbit: what happens if the type has non-trivial destruction? – peppe Jul 08 '16 at 16:52
  • @peppe: Then you have to do what templatetypedef said (IIRC this is otherwise optional) – Lightness Races in Orbit Jul 08 '16 at 16:54

1 Answers1

4

You can reuse the storage after the lifetime of the object has ended. The lifetime ends with the destructor call. There's nothing technically problematic in that.

Using the object after its lifetime has ended, as the presented example code did at the time I wrote this answer in

pt.~point2d();
new (&pt) point2d { pt.x + value, pt.y };

is Undefined Behavior.

If you insist on using the point class with const fields, you can work around that like this:

void move_x( int const value )
{
     auto const old_pt = pt;
     pt.~point2d();
     ::new (&pt) point2d { old_pt.x + value, old_pt.y };
}

That may feel like unnecessary complication and possible micro-inefficiency, but rather, the unnecessary complication is the point class.

Cheers and hth. - Alf
  • 142,714
  • 15
  • 209
  • 331
  • @Dan: At a guess your compiler will completely optimize away that. This is all about the formal, telling the compiler that it can't do too clever things based on assumptions that hold only because of formal UB otherwise. – Cheers and hth. - Alf Jul 08 '16 at 16:38
  • Also IIRC all pointers and references become invalid with destruction and do not return to the valid state after reconstruction so you will nedd to update them. Don't have access to standard now. – Revolver_Ocelot Jul 08 '16 at 16:39
  • 2
    @Dan, While this is a technically correct answer to your particular question, pay most attention to the last line of the response "That may feel like unnecessary complication and possible micro-inefficiency, but rather, the unnecessary complication is the point class." There other ways to handle this that are much better. Using "clever" tricks like this usually result in ridicule years down the line and people swearing vengeance against you and your loved ones. – Andrew Jul 08 '16 at 16:43
  • 1
    I guess this `move_x` implementation is roughly what a "replacing emplacement" operation on a container would do. – peppe Jul 08 '16 at 16:43
  • 1
    If there is a hard requirement that you cannot modify that point2d class definition, then write your own point2d class. Use that class wherever possible and convert to the junk point2d class at the last possible opportunity. It seems like it's supposed to the used as a "view" anyway. – Andrew Jul 08 '16 at 16:57