13
votes

According to the C++ standard, a class that has virtual functions cannot have a trivial copy constructor:

A copy/move constructor for class X is trivial if it is not user-provided and if

— class X has no virtual functions (10.3) and no virtual base classes (10.1), and

— the constructor selected to copy/move each direct base class subobject is trivial, and

— for each non-static data member of X that is of class type (or array thereof), the constructor selected to copy/move that member is trivial;

otherwise the copy/move constructor is non-trivial.

Now, imagine a class hierarchy that satisfies all the mentioned conditions except the 'no virtual functions' condition:

struct I
{
    virtual void f() = 0;
};

struct D final : I
{
   void f() override 
   {
   }
};

From an implementation's point of view those classes contain only a pointer to the virtual dispatch table. And the base class does not have any user-declared constructors, and the derived class is final, so the mentioned pointer can always have a constant value. Given all that, the copy constructor can be trivial, yet the standard explicitly forbids treating it as such.

On the other hand, the conditions of treating such classes as trivially destructible are met. The class does not declare a virtual destructor and uses implicitly-defined destructors.

Requiring triviality of destructors looks like mandating an optimization - implicitly defined destructors should not restore pointers to the virtual table in this case.

But such an optimization goes half-way in my opinion; classes with virtual functions still cannot be memcopied, even if they can be trivially destructed.

The questions:

  1. Are there any reasons I did not think of from an implementation perspective why such classes should have non-trivial copy-constructors?

  2. Are there any reasons the restrictions on triviality for copy-constructors cannot be relaxed in the standard?

3
They aren’t required, thw constructor is defined as being non trivial.user2672107
Yes, in that sense cctor still can have trivial implementation, a compiler may still do a memcpy, when copying such objects, the standard does not preclude that. But user cannot.Григорий Шуренков
It probably has something to do with the language rule that virtual functions are not yet virtual in the constructor. Fairly tricky to do, it requires changing the v-table pointer. Last thing that happens in the constructor is changing it to the permanent one so virtual function calls now behave virtually. A memcopy could screw that up by copying that pointer too early.Hans Passant

3 Answers

8
votes

Are there any reasons I did not think of from implementation perspective why such classes should have non-trivial copy-constructors?

There is quite obvious one: copy-constructor of I is not trivial. And it is not final, so there can be other derived classes. So it must be non-trivial and set virtual table pointer properly after memcpy, as there could be derived classes relying on it.

Are there any reasons the restrictions on triviality for copy-constructors cannot be relaxed in the standard?

1) Constructor triviality part was simply not revised with inclusion of final keyword.

2) People think that keywords like delete, final and overrride should help avoid most common errors, and clarify programmer intention, not change behavior of the program.

3) It complicates language: A constructor is trivial, unless you have virtual function, then it is nontrivial, unless your class is final, then it is trivial again, unless something else, then it is not, unless...

4) Nobody though that it is worth writing formal paper for, proving usefulness of this addition to Committee and pushing this change into language.

6
votes

The copies and moves of polymorphic classes cannot be trivial because it would break slicing copies and moves which copy a base type of an object's dynamic type. For example:

struct Base { virtual int f() { return 0; } };
struct D1 : Base { int f() override { return 1; } };
struct D2 : Base { int f() override { return 2; } };

int main() {
  D1 d1;
  D2 d2;
  Base b = d1; // if this copy constructor were trivial, b would have D1's vtable
  b.f();       // ...and this call would return 1 instead of 0.
  b = d2;      // Ditto: b would have D2's vtable
  b.f();       // ...and this call would return 2 instead of 0.
}
-2
votes

Trivial constructors means that no additional effort is needed. So for copy c-tor/assigment operator it means simple memcpy. Virtual functions as you mentioned are creating vtable, which is table of pointers to functions. So if you'd try to copy memory of D object, you'll also copy its vtable. New object would have vtable with pointers pointing to old memory. This would crearly not an ideal situation.