3

Copy constructors were traditionally ubiquitous in C++ programs. However, I'm doubting whether there's a good reason to that since C++11.

Even when the program logic didn't need copying objects, copy constructors (usu. default) were often included for the sole purpose of object reallocation. Without a copy constructor, you couldn't store objects in a std::vector or even return an object from a function.

However, since C++11, move constructors have been responsible for object reallocation.

Another use case for copy constructors was, simply, making clones of objects. However, I'm quite convinced that a .copy() or .clone() method is better suited for that role than a copy constructor because...

  1. Copying objects isn't really commonplace. Certainly it's sometimes necessary for an object's interface to contain a "make a duplicate of yourself" method, but only sometimes. And when it is the case, explicit is better than implicit.

  2. Sometimes an object could expose several different .copy()-like methods, because in different contexts the copy might need to be created differently (e.g. shallower or deeper).

  3. In some contexts, we'd want the .copy() methods to do non-trivial things related to program logic (increment some counter, or perhaps generate a new unique name for the copy). I wouldn't accept any code that has non-obvious logic in a copy constructor.

  4. Last but not least, a .copy() method can be virtual if needed, allowing to solve the problem of slicing.


The only cases where I'd actually want to use a copy constructor are:

  • RAII handles of copiable resources (quite obviously)
  • Structures that are intended to be used like built-in types, like math vectors or matrices -
    simply because they are copied often and vec3 b = a.copy() is too verbose.

Side note: I've considered the fact that copy constructor is needed for CAS, but CAS is needed for operator=(const T&) which I consider redundant basing on the exact same reasoning;
.copy() + operator=(T&&) = default would be preferred if you really need this.)

For me, that's quite enough incentive to use T(const T&) = delete everywhere by default and provide a .copy() method when needed. (Perhaps also a private T(const T&) = default just to be able to write copy() or virtual copy() without boilerplate.)

Q: Is the above reasoning correct or am I missing any good reasons why logic objects actually need or somehow benefit from copy constructors?

Specifically, am I correct in that move constructors took over the responsibility of object reallocation in C++11 completely? I'm using "reallocation" informally for all the situations when an object needs to be moved someplace else in the memory without altering its state.

Community
  • 1
  • 1
Kos
  • 70,399
  • 25
  • 169
  • 233
  • 3
    "Copying objects isn't really commonplace." - I'd say it's pretty common considering that C++ uses value semantics by default. Two objects with the same value should represent the same thing. – Joseph Mansfield May 08 '13 at 19:04
  • 3
    Disabling copying by default might arguably have some merits; but why on earth would you enable it through a member function rather than the idiomatic use of copy constructors? – Mike Seymour May 08 '13 at 19:09
  • @sftrabbit: not necessarily: values are not polymorphic, references and pointers are. If you want runtime polymorphism in C++ you identify the objects by their addresses, not values (which represent just "state", not "identity"). Two `Person`-s having a same `m_name` are not the same `Person`. – Emilio Garavaglia May 09 '13 at 06:28
  • @sftrabbit I'm aware of that, but having your OO design rely on objects with meaningful identity is very common nowadays. – Kos May 09 '13 at 06:32
  • @MikeSeymour I believe I've given 4 reasons why, only one of them being somewhat subjective IMO. Feel free to disagree, but please relate to them when explaining your POV – Kos May 09 '13 at 06:33
  • I don't know who downvoted or voted to close this, but it's a perfectly valid question. – Puppy May 09 '13 at 08:40
  • @Kos: Apologies, I should have refrained from expressing my opinion until I had time to back it up properly. I'll write a proper response soon. – Mike Seymour May 09 '13 at 10:23

4 Answers4

5

The problem is what is the word "object" referring to.

If objects are the resources that variables refers to (like in java or in C++ through pointers, using classical OOP paradigms) every "copy between variables" is a "sharing", and if single ownership is imposed, "sharing" becomes "moving".

If objects are the variables themselves, since each variables has to have its own history, you cannot "move" if you cannot / don't want to impose the destruction of a value in favor of another.

Cosider for example std::strings:

   std::string a="Aa";
   std::string b=a;
   ...
   b = "Bb";

Do you expect the value of a to change, or that code to don't compile? If not, then copy is needed.

Now consider this:

   std::string a="Aa";
   std::string b=std::move(a);
   ...
   b = "Bb";

Now a is left empty, since its value (better, the dynamic memory that contains it) had been "moved" to b. The value of b is then chaged, and the old "Aa" discarded.

In essence, move works only if explicitly called or if the right argument is "temporary", like in

  a = b+c;

where the resource hold by the return of operator+ is clearly not needed after the assignment, hence moving it to a, rather than copy it in another a's held place and delete it is more effective.

Move and copy are two different things. Move is not "THE replacement for copy". It an more efficient way to avoid copy only in all the cases when an object is not required to generate a clone of itself.

Emilio Garavaglia
  • 20,229
  • 2
  • 46
  • 63
  • Thanks! Objects are certainly different from variables, but C++ gives the rare (and useful) option for the variable's *value* to be the object - unifying the object's storage with the variable's. Your example shows 2 variables of value type, which means that 2 actual, distinct objects should be there, one perhaps being a copy of the other. And yes, I'd prefer `string b=a` to fail at compile time and `string b=a.copy()` to succeed. – Kos May 09 '13 at 06:39
  • @Kos: You actually CAN do that. But is not something C++ impose you to do. If you want a framework where all objects are on heap, and only reference wrappers / smart pointers are on stack you can certainly do that. But this is not that much different from java. So why don't just use Java ? Don't answer. Just think about and decide. Context -in this case- matters more than anything else. But by a language stand point, those "reference wrappers" or "smart pointer" are themselves ... value classes. So the language cannot do without it! – Emilio Garavaglia May 09 '13 at 06:45
  • @Kos: from an idiomatic stand point, `strring b=copy(a)` should probably be better pair with `string b=move(a)`. Copy and Move paly the role of "manipulators", rather than "members". – Emilio Garavaglia May 09 '13 at 06:50
  • Have you perhaps gotten the impression that I'm against using value types in C++? That's not the case. Value types are central to C++ and I'd never give them up. The convention I'm proposing still allows you to consistently work with objects of any storage, and I don't believe it introduces any limitations on design, expressiveness or performance. I'm trying to confirm (or disprove) that. – Kos May 09 '13 at 06:56
  • @Kos: "Have you perhaps gotten the impression that I'm against using value types in C++?" Not at all. I was just making evident the differences the two styles will lead, so than whatever decision can have more elements to be based on. Sorry if it was unclear! – Emilio Garavaglia May 09 '13 at 07:01
  • Whether `copy` should be a free function- hmm... copy *is* a distinct part of an object's interface, rather than being written in terms of its interface. If you inherit, it allows you to leverage return type covariance. Also take note that "copying an object" isn't guaranteed to look and mean the same in different contexts or for different objects (points 2 & 3 in my Q). OTOH, free functions (friend functions) looked up through ADL are really common in C++ and may be considered part of the object's interface too... I'd need to think more here. :-) – Kos May 09 '13 at 07:02
3

Short anwer

Is the above reasoning correct or am I missing any good reasons why logic objects actually need or somehow benefit from copy constructors?

Automatically generated copy constructors are a great benefit in separating resource management from program logic; classes implementing logic do not need to worry about allocating, freeing or copying resources at all.

In my opinion, any replacement would need to do the same, and doing that for named functions feels a bit weird.

Long answer

When considering copy semantics, it's useful to divide types into four categories:

  • Primitive types, with semantics defined by the language;
  • Resource management (or RAII) types, with special requirements;
  • Aggregate types, which simply copy each member;
  • Polymorphic types.

Primitive types are what they are, so they are beyond the scope of the question; I'm assuming that a radical change to the language, breaking decades of legacy code, won't happen. Polymorphic types can't be copied (while maintaining the dynamic type) without user-defined virtual functions or RTTI shenanigans, so they are also beyond the scope of the question.

So the proposal is: mandate that RAII and aggregate types implement a named function, rather than a copy constructor, if they should be copied.

This makes little difference to RAII types; they just need to declare a differently-named copy function, and users just need to be slightly more verbose.

However, in the current world, aggregate types do not need to declare an explicit copy constructor at all; one will be generated automatically to copy all the members, or deleted if any are uncopyable. This ensures that, as long as all the member types are correctly copyable, so is the aggregate.

In your world, there are two possibilities:

  • Either the language knows about your copy-function, and can automatically generate one (perhaps only if explicitly requested, i.e. T copy() = default;, since you want explicitness). In my opinion, automatically generating named functions based on the same named function in other types feels more like magic than the current scheme of generating "language elements" (constructors and operator overloads), but perhaps that's just my prejudice speaking.
  • Or it's left to the user to correctly implement copying semantics for aggregates. This is error-prone (since you could add a member and forget to update the function), and breaks the current clean separation between resource management and program logic.

And to address the points you make in favour:

  1. Copying (non-polymorphic) objects is commonplace, although as you say it's less common now that they can be moved when possible. It's just your opinion that "explicit is better" or that T a(b); is less explicit than T a(b.copy());
  2. Agreed, if an object doesn't have clearly defined copy semantics, then it should have named functions to cover whatever options it offers. I don't see how that affects how normal objects should be copied.
  3. I've no idea why you think that a copy constructor shouldn't be allowed to do things that a named function could, as long as they are part of the defined copy semantics. You argue that copy constructors shouldn't be used because of artificial restrictions that you place on them yourself.
  4. Copying polymorphic objects is an entirely different kettle of fish. Forcing all types to use named functions just because polymorphic ones must won't give the consistency you seem to be arguing for, since the return types would have to be different. Polymorphic copies will need to be dynamically allocated and returned by pointer; non-polymorphic copies should be returned by value. In my opinion, there is little value in making these different operations look similar without being interchangable.
Mike Seymour
  • 249,747
  • 28
  • 448
  • 644
  • Thanks, Mike! I value auto copy ctors as an aid in writing either trivial or non-trivial copy routines. My suggestion was to 1) leave custom copy ctors to RAII types, 2) leave public default copy ctors to aggregates, 3) provide copy() or virtual copy() instead of a public copy ctor everywhere else. One thing I've overlooked is the requirement for polymorphic copies to rely on dynamically allocated memory. I can now see that providing named copy methods would still fail to unify copying interface between polymorphic and non-polymorphic non-aggregates (or "logic objects" as I've said before). – Kos May 11 '13 at 14:51
  • A suggestion I still stand for is using a named method if the program logic requires an object to be copied in a non-trivial way. In such case, I'd still insist to have a named method and no public copy ctor. Why? It's somewhat easy to make the mistake and use the copy ctor instead of a move ctor when the sole intent is reallocation, not logic-related duplication. It can be as easy as omitting one "noexcept". Copy constructors are still tricky because of their dual role (logic-related and reallocation-related) - this fact led me into this whole train of thought. – Kos May 11 '13 at 14:52
  • This actually has an interesting continuation - is *any* logic-related copy of a non-aggregate actually "trivial"? Perhaps not; I'd need to digest that. I'd like to get back to you if I come up with a convincing example. – Kos May 11 '13 at 15:01
1

One case where copy constructors come in useful is when implementing the strong exception guarantees.

To illustrate the point, let's consider the resize function of std::vector. The function might be implemented roughly as follows:

void std::vector::resize(std::size_t n)
{
    if (n > capacity())
    {
        T *newData = new T [n];
        for (std::size_t i = 0; i < capacity(); i++)
            newData[i] = std::move(m_data[i]);
        delete[] m_data;
        m_data = newData;
    }
    else
    { /* ... */ }
}

If the resize function were to have a strong exception guarantee we need to ensure that, if an exception is thrown, the state of the std::vector before the resize() call is preserved.

If T has no move constructor, then we will default to the copy constructor. In this case, if the copy constructor throws an exception, we can still provide strong exception guarantee: we simply delete the newData array and no harm to the std::vector has been done.

However, if we were using the move constructor of T and it threw an exception, then we have a bunch of Ts that were moved into the newData array. Rolling this operation back isn't straight-forward: if we try to move them back into the m_data array the move constructor of T may throw an exception again!

To resolve this issue we have the std::move_if_noexcept function. This function will use the move constructor of T if it is marked as noexcept, otherwise the copy constructor will be used. This allows us to implement std::vector::resize in such a way as to provide a strong exception guarantee.

For completeness, I should mention that C++11 std::vector::resize does not provide a strong exception guarantee in all cases. According to www.cplusplus.com we have the the follow guarantees:

If n is less than or equal to the size of the container, the function never throws exceptions (no-throw guarantee). If n is greater and a reallocation happens, there are no changes in the container in case of exception (strong guarantee) if the type of the elements is either copyable or no-throw moveable. Otherwise, if an exception is thrown, the container is left with a valid state (basic guarantee).

user2093113
  • 3,230
  • 1
  • 14
  • 21
  • Thanks, that's an important point you've raised here. But is it really normal (or: is it ever correct) to have throwing `swap`, move ctors and move op=s? I've even seen a [proposal](http://www.open-std.org/jtc1/sc22/wg21/docs/papers/2009/n2855.html#nothrowmove) to make nothrow mandatory here. – Kos May 09 '13 at 07:05
  • @Kos: Unfortunately, it is in the current state of C++. – Puppy May 09 '13 at 08:41
1

Here's the thing. Moving is the new default- the new minimum requirement. But copying is still often a useful and convenient operation.

Nobody should bend over backwards to offer a copy constructor anymore. But it is still useful for your users to have copyability if you can offer it simply.

I would not ditch copy constructors any time soon, but I admit that for my own types, I only add them when it becomes clear I need them- not immediately. So far this is very, very few types.

Puppy
  • 144,682
  • 38
  • 256
  • 465