Reputation: 3235
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.
Upvotes: 7
Views: 1054
Reputation: 69882
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;
}
Upvotes: 2
Reputation: 64925
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.
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.
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.
Upvotes: 2
Reputation: 4641
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.
Upvotes: 1