Why Aren't Copy Constructors "Chained" Like Default Constructors and Destructors

Why aren't copy constructors chained like default constructors and destructors?

The copy constructor doesn't (can't) really make a copy of the object because Derived::Derived(const Derived&) can't access pdata to change it.

Sure it can:

Derived(const Derived& d)
: Base(d)
{
cout << "Derived::Derived(const B&)" << endl;
}

If you don't specify a base class constructor in the initializer list, its default constructor is called. If you want a constructor other than the default constructor to be called, you must specify which constructor (and with which arguments) you want to call.

As for why this is the case: why should a copy constructor be any different from any other constructor? As an example of a practical problem:

struct Base
{
Base() { }
Base(Base volatile&) { } // (1)
Base(Base const&) { } // (2)
};

struct Derived : Base
{
Derived(Derived&) { }
};

Which of the Base copy constructors would you expect the Derived copy constructor to call?

Why are copy constructors of base classes not implicitly called?

Why are copy constructors different from normal constructors in this regard?

They aren't. When you do not call a base class constructor, the default constructor is called for you. This happens for every contructor.

Why does the implicit copy constructor calls the base class copy constructor and the defined copy constructor doesn't?

That's just the way the implicit copy constructor is defined (it wouldn't make sense calling the default). As soon as you define any constructor (copy or otherwise) its normal automatic behavior is to call the default parent constructor, so it would be inconsistent to change that for one specific user-defined constructor.

Why aren't copy constructors chained like default constructors and destructors?

The copy constructor doesn't (can't) really make a copy of the object because Derived::Derived(const Derived&) can't access pdata to change it.

Sure it can:

Derived(const Derived& d)
: Base(d)
{
cout << "Derived::Derived(const B&)" << endl;
}

If you don't specify a base class constructor in the initializer list, its default constructor is called. If you want a constructor other than the default constructor to be called, you must specify which constructor (and with which arguments) you want to call.

As for why this is the case: why should a copy constructor be any different from any other constructor? As an example of a practical problem:

struct Base
{
Base() { }
Base(Base volatile&) { } // (1)
Base(Base const&) { } // (2)
};

struct Derived : Base
{
Derived(Derived&) { }
};

Which of the Base copy constructors would you expect the Derived copy constructor to call?

Why should copy constructors be sometimes declared explicitly non-inlined?

Binary compatibility for dynamic libraries (.dll, .so) is often an important thing.

e.g. you don't want to have to recompile half the software on the OS because you updated some low level library everything uses in an incompatible way (and consider how frequent security updates can be). Often you may not even have all the source code required to do so even if you wanted.

For updates to your dynamic library to be compatible, and actually have an effect, you essentially can not change anything in a public header file, because everything there was compiled into those other binaries directly (even in C code, this can often include struct sizes and member layouts, and obviously you cant remove or change any function declarations either).

In addition to the C issues, C++ introduces many more (order of virtual functions, how inheritance works, etc.) so it is conceivable that you might do something that changes the auto generated C++ constructor, copy, destructor etc. while otherwise maintaining compatibility. If they are defined "inline" along with the class/struct, rather than explicitly in your source, then they will have been included directly by other applications/libraries that linked your dynamic library and used those auto generated functions, and they wont get your changed version (which you maybe didn't even realise has changed!).

C++ Default copy constructor

Any copy constructor declared in the class (be it private, public or protected) means the compiler will not generate a default copy ctor. Whether the one declared in the class is then also defined or not only controls whether code with the proper level of visibility into it can copy instances of the class (if not defined, the linker will complain; the compiler's job is only to complain about use without proper visibility, not to duplicate the linker's job).

For example, if you declare a private copy ctor, only code that is in functions in the class (or friends, of course) is allowed to compile if it tries copying an instance. If the ctor is not defined, that code, however, will not survive the linker, so you get an error anyway (just unfortunately a bit later in the build process, i.e. possibly with a modest waste of computational resources at build time compared with earlier-detected errors).

Can we say bye to copy constructors?

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.


Related Topics



Leave a reply



Submit