6

I believe this is a common problem, but some googling does not return match, hence ask here.

So I have the following class:

class A {
  public:
    A(const A &rhs) { m_a = rhs.m_a; }

   private:
      int m_a;
};

Everything is cool until some time later, could be a year later, I add a new property, m_b to class A, but I forget to update the copy constructor.

It takes a painful debug to locate the out-of-sync.

Is there a trick to avoid such a problem, ideally at build time?

Yes, I can write unit test to cover that copy constructor, but when I forget updating copy constructor, most likely I forget that unit test also.

BeeOnRope
  • 60,350
  • 16
  • 207
  • 386
my_question
  • 3,075
  • 2
  • 27
  • 44
  • 1
    Don't write a copy constructor - there is no need for a user-defined one for that class or for most classes. –  Oct 15 '17 at 18:40
  • Your code has various typos/bugs in it. Ignoring that, defaulted constructors is your friend – Passer By Oct 15 '17 at 18:42
  • 1
    You can write `constexpr static auto members(){ return std::make_tuple(&A::m_a); }` which you use everywhere where you need the list of members and you can later extend it when needed. There is a dupe on the site somewhere that explains the details. The better way in this case though is to stick to the *rule of zero*. – nwp Oct 15 '17 at 18:46
  • On the specific topic of unit tests for this kind of stuff, you could always have a `sizeof()` check in the unit test which would fail if a new member was added, in order to flag it as needing updating. There are also some tricks you can play by `memcpy`ing the representation of the objects and checking the results between two objects after calling the copy constructor, although padding often gets in the way here (i.e,. it can be hard to tell the difference between padding, which doesn't getting overwritten, and forgetting to copy a member). – BeeOnRope Oct 15 '17 at 21:31

3 Answers3

2

The best approach is probably to rely on the default copy constructor. For instance, your particular example, which involves a member-wise copy, works fine with the default constructor (i.e., the behavior would be the same even if you simply deleted the constructor). As more members are added, they will automatically receive the same member-wise copy behavior in the default constructor.

In some unusual cases you might want to force the generation of the default constructor (e.g., when you want to have some different, explicitly defined behavior for non-const source objects). In that case, in C++11 and later, you can explicitly request the default copy constructor as follows:

A(const A&) = default;

Some coding guidelines also recommend always including the above explicit request for the default constructor simply as a form of documentation.

Mostly member-wise, with exceptions

Sometimes, most member of a class are fine with the default member-wise copy, but you have a couple of exceptions. An example would be a raw pointer where you want to perform a deep copy of the underlying data. By default, the pointer is simply copied and so the pointers in the source object and new object will both point to the same location in memory.

The solution is fairly simple: just wrap this pointer and any associated meta-data (e.g., a length field if the pointed to object is an array) in a suitable RAII wrapper whose copy constructor performs the specific non-default behavior you want, and include a member of this type in your class A. Now A can continue to use the default copy constructor, which calls your explicit copy constructor for your pointer. Essentially, you are back to the pure member-wise copy, but with the new semantics for your pointer-wrapping member.

This type of matter will also help you keep your destructor and sometimes your constructors trivial as well. No doubt the original class above had some code to delete your raw pointer. Once wrapped with the copying-RAII wrapper, the wrapper takes care of destruction and so the containing class doesn't have to, and often the destructor can be removed entirely.

This wrapper approach often has zero or close to zero overhead in a language like C++.

You can find some more details in this question.

When that doesn't work

Sometimes the above may not work. One example would be when the copy constructor needs to embed the address (the this pointer) of the new object in some field. The self-contained wrapper doesn't have a way to get the this pointer of the containing object (indeed, it doesn't even know it is a member).

One approach here is to create a new sub-object with all of the fields of your original object that use the default copy behavior, and then create your new top level object by aggregating (or by inheritance) this sub-object with the few fields that do need special treatment. Then you can keep the using the default copy constructor for all the fields that should have the default treatment.

This approach can even have performance benefits, since compilers are likely to use a pure memcpy approach1 for the sub-object, in addition to calling your explicit code for the exceptional fields. When the fields were mingled together in the original object, this is much less likely or impossible (e.g., because the layout of the object may interleave the exceptional fields and default-copied fields).


1 This doesn't actually mean that there will be a call to the standard library memcpy in the code: for small objects the compiler will usually unroll this for small objects into an unrolled sequence of mostly maximum width loads and stores.

BeeOnRope
  • 60,350
  • 16
  • 207
  • 386
2

The only time you ever need to write copy constructors, assignment operators or destructors is if your class is an RAII wrapper for exactly one resource.

If your class is managing more than one resource, it's time to refactor it so that it's composed of classes which manage exactly one resource.

example:

#include <algorithm>

// this class is managing two resources. 
// This will be a maintenance nightmare and will required
// horribly complicated constructor code.
struct DoubleVector
{
    int *vec1;
    int *vec2;
};


// this class manages a single resource

struct SingleVector
{
    SingleVector() : vec (new int[10]) {}
    SingleVector(SingleVector const& other) 
    : vec (new int[10]) 
    {
        // note - simple constructor

        std::copy(other.vec, other.vec + 10, vec);
    }

    SingleVector& operator=(SingleVector const& other) 
    {
        auto temp = other;
        std::swap(vec, temp.vec);
        return *this;
    }

    ~SingleVector() {
        delete [] vec;
    }

    // note - single resource
    int *vec;
};

// this class uses *composition* and default assignment/construction/destruction
// it will never go wrong. And it could not be simpler.
struct DoubleVectorDoneRight
{
    SingleVector vec1;
    SingleVector vec2;
};

int main()
{
    SingleVector v;
    SingleVector v2 = v;

    SingleVector v3;
    v3 = v;
}
Richard Hodges
  • 68,278
  • 7
  • 90
  • 142
1

Turn on all your compiler warnings. You should hopefully get a warning about any uninitialized member variables. On GCC the specific flag for this is -Weffc++. See this StackOverflow post for more info.

Also, assign your values in the initializer list and not the constructor body whenever possible.

GraphicsMuncher
  • 4,583
  • 4
  • 35
  • 50