Assignment and recovery operator: if I am careful?

This is a bad template. Copy and change is better.

foo & operator = ( foo const & other ) { static_assert ( noexcept( new (this) foo() ), "Exception safety violation" ); this-> ~ foo(); try { new (this) foo( other ); } catch (...) { new (this) foo(); // does not throw throw; } return * this; } 

While foo not polymorphic , what could go wrong? (However, suppose this is a base class.)

Background: I am dealing with erasing the type of local storage, and an alternative is to implement swap as two hard-coded destinations through the local storage space. The objects in the memory blocks of the source and destination are of different types and simply cannot exchange with each other. Copying / moving a construct defined in terms of such a swap is twice as difficult for the apparent lack of amplification.

+6
source share
2 answers

Destruction and restoration has a fundamentally different behavior from copying and replacing. For comparison:

  • Destroy the old value. He disappeared forever.
  • Try creating a new value.
  • If necessary, discard and create a default value.

Copy and replace:

  • Try creating a new value.
  • Discard the old value if necessary.
  • Apply new value.

Both have their merits, but copying and swap are ubiquitous, so its flaws are discussed on the principle of least surprise. Therefore, let him emulate his behavior:

 foo & operator = ( foo const & other ) { static_assert ( std::is_nothrow_move_constructible< foo >::value || std::is_nothrow_default_constructible< foo >::value , "Exception safety violation" ); foo next( other ); try { this-> ~ foo(); new (this) foo( std::move( next ) ); } catch (...) { new (this) foo(); throw; } return * this; } 

Although it is more complex, it is better than throwing a swap , which after an exception can leave a mishmash of old and new values.

In the general case, when the move constructor does not throw (you remember its noexcept , right?), The algorithm nicely terminates:

 foo & operator = ( foo const & other ) { foo next( other ); // The dangerous part is over now. this-> ~ foo(); new (this) foo( std::move( next ) ); return * this; } 
0
source

The standard provides guarantees for such interrupted lifetimes in [basic.life] ยง3.8 / 7:

If, after the object has reached the end of its life and before the storage that the object is busy reusing or released, a new object is created in the storage location where the original object was loaded, a pointer pointing to the original object, a link related to the original object, or the name of the source object will automatically refer to the new object and, as soon as the lifetime of the new object is started, it can be used to manage the new object if:

- the storage for the new object exactly overlays the storage location that was occupied by the original object, and

- the new object is of the same type as the original object (ignoring the top-level cv qualifiers) and

- the type of the source object is not a const qualifier and, if the class type does not contain a non-static data member whose type is constant or reference, and

- the original object was the most derived object (ยง 1.8) of type T , and the new object is the most derived object of type T (that is, they are not subobjects of the base class).

The last paragraph disqualifies my use case. However, since this applies to only one non-polymorphic class, it also allows you to turn the destructor and constructors into private destroy and init member functions, respectively.

In other words, when destroy-and-regenerate is legal, you can also do the same using member functions, and not new / delete .

The โ€œadvantageโ€ in this is that he ceases to look smart, so no uninformed passerby will want to copy the project.

+2
source

Source: https://habr.com/ru/post/987946/


All Articles