1

I have a class with own resource management:

class Lol
{

private:
  // This is data which this class allocates
  char *mName = nullptr;

public:
    Lol(std::string str) // In constructor just copy data from string
    {
        auto cstr = str.c_str();
        auto len = strlen(cstr);
        mName = new char[len + 1];
        strcpy(mName, cstr);
    };

    ~Lol() // And of course clean up
    {
        delete[] mName;
    }
}

I implemented copy constructor which just copies managed data:

    Lol(const Lol &other)
    {
        auto cstr = other.mName;
        auto len = strlen(cstr);
        mName = new char[len + 1];
        strcpy(mName, cstr);
    };

I also need to implement copy assignment operator. I just did this:

    Lol &operator=(const Lol &other)
    {
        if (this == &other)
        {
            return *this;
        }

        // Clean up my resources
        this->~Lol();
 
        // And copy resources from "other" using already implemented copy constructor
        new (this) Lol(other);
    }

Looks like this copy assignment operator will work for all classes. Why do I need to have another code in the copy assignment operator? What is use case for it?

Vasilii Rogin
  • 157
  • 1
  • 9
  • 1
    If your class has const or reference members then the placement new solution does no work. – NathanOliver Apr 20 '23 at 20:01
  • Also, if the constructor throws an exception, then your app will have undefined behavior and there's no way to fix it – Mooing Duck Apr 20 '23 at 20:09
  • `this->~Lol(); new (this) Lol(other);` -- I have to ask -- given such a simple class, where did the idea to do this come from all of a sudden? What's the reason for using placement-new? Why not simply: `{ Lol temp(other); std::swap(mName, temp.mName); return *this; }` – PaulMcKenzie Apr 20 '23 at 20:09
  • 1
    With proper RAII, this problem goes away. `std::unique_ptr` or `std::vector` or.... `std::string` – Mooing Duck Apr 20 '23 at 20:10
  • Having a move constructor and move assignment also means copy-assignment is a trivial copy-and-swap. – Mooing Duck Apr 20 '23 at 20:11
  • You should read : [use-a-unique_ptrt-to-hold-pointers](https://isocpp.github.io/CppCoreGuidelines/CppCoreGuidelines#es24-use-a-unique_ptrt-to-hold-pointers) from the C++ core guidelines – Pepijn Kramer Apr 20 '23 at 20:31
  • Eliminate these kinds of issues, use `std::string` and other data structures that manage their own memory. – Thomas Matthews Apr 20 '23 at 20:50
  • Look up Copy and Swap Idiom. Basically the same but from a different angle (uses Copy constructor and destructor, but in a different order). But this idiom is exception safe. – Martin York Apr 20 '23 at 21:45

2 Answers2

2

If the constructor throws, you'd have to catch the exception and somehow recover (by calling some constructor, probably by the default one), which makes this much less elegant. Failing to call a constructor will lead to a double destruction and UB.

Also this approach leads to UB if you inherit from such class, or use it as a [[no_unique_address]] member variable:

[basic.life]/8.4

An object o1 is transparently replaceable by an object o2 if: ...

— neither o1 nor o2 is a potentially-overlapping subobject ...

and [intro.object]/7

A potentially-overlapping subobject is either:

— a base class subobject, or

— a non-static data member declared with the no_unique_address attribute.

This is not UB per se, but if your object isn't transparently replaceable, the reconstructed object must be std::laundered before use, which isn't practical (e.g. if it's an automatic variable, the automatic destruction will happen without std::launder → UB).

C++17 had even more restrictions. You also needed std::launder if your class contained const or reference members (C++17 [basic.life]/8.3).


If you're looking for an universal assignment operator, there is one. It's called the copy&swap idiom. Behold:

MyClass &operator=(MyClass other) noexcept
{
    std::swap(x, other.x); // Swap every member here.
    return *this;
}

This acts as both copy and move assignment (if you have the respective constructors), and offers the strong exception guarantee (if the copy throws, the target object is unchanged).

The only case (that I know) where it doesn't work out of the box is when the class maintains a pointer to itself somewhere (possibly inside itself).

HolyBlackCat
  • 78,603
  • 9
  • 131
  • 207
  • Why it works? If i have this code: `Lol lol1{"lol1"}; Lol lol2{"lol2"}; lol1 = lol2;` Here I call copy assignment operator. I would expect here that both lol1 and lol2 have different buffers with the same data. But if I swap then lol2 will have "lol1", is not it? – Vasilii Rogin Apr 20 '23 at 20:25
  • @VasiliiRogin Note that the parameter is passed by value. So first `lol2` is copied (in this case) or moved into the parameter, then the parameter is swapped with `lol1`, and then the parameter is destroyed (holding the old value of `lol1`). – HolyBlackCat Apr 20 '23 at 20:27
  • Copy/swap idiom is the 1st solution to copyable resource managing classes. It works great most often. There are cases where you can find better optimized alternatives; but copy/swap idiom is a pattern that generally leads to functionally correct code. – Red.Wave Apr 20 '23 at 21:21
1

Use the copy and swap Idiom.

Lol &operator=(const Lol &other)
{
    if (this == &other)
    {
        return *this;
    }

    // Clean up my resources
    this->~Lol();

    // Copy can throw.
    // Then your object is in an undefined state.
    new (this) Lol(other);

    // You forgot the return:
    return *this;
}

So this does not provide the strong (or any) exception gurantees.

The preferred way would be:

Lol& operator=(Lol const& other)
{
    Lol   copy(other);         // Here we use the copy constructor
                               // And the destructor at the end of
                               // function cleans up the scope
                               // Note this happens after the swap
                               // so you are cleaning up what was in
                               // this object.
    swap(copy);
    return *this;
}
void swap(Lol& other) noexcept
{
    std::swap(mName, other.mName);
}

Nowadays we have improved on this original

Lol& operator=(Lol copy)      // Notice we have moved the copy here.
{
    swap(copy);
    return *this;
}

The exciting thing here is that this version of assignment works for both copy and move assignment just as effeciently.

Martin York
  • 257,169
  • 86
  • 333
  • 562