What Is a Non-Trivial Constructor in C++

What is a non-trivial constructor in C++?

In simple words a "trivial" special member function literally means a member function that does its job in a very straightforward manner. The "straightforward manner" means different thing for different kinds of special member functions.

For a default constructor and destructor being "trivial" means literally "do nothing at all". For copy-constructor and copy-assignment operator, being "trivial" means literally "be equivalent to simple raw memory copying" (like copy with memcpy).

If you define a constructor yourself, it is considered non-trivial, even if it doesn't do anything, so a trivial constructor must be implicitly defined by the compiler.

In order for a special member function to satisfy the above requirements, the class must have a very simplistic structure, it must not require any hidden initializations when an object is being created or destroyed, or any hidden additional internal manipulations when it is being copied.

For example, if class has virtual functions, it will require some extra hidden initializations when objects of this class are being created (initialize virtual method table and such), so the constructor for this class will not qualify as trivial.

For another example, if a class has virtual base classes, then each object of this class might contain hidden pointers that point to other parts of the very same object. Such a self-referential object cannot be copied by a simple raw memory copy routine (like memcpy). Extra manipulations will be necessary to properly re-initialize the hidden pointers in the copy. For this reason the copy constructor and copy-assignment operator for this class will not qualify as trivial.

For obvious reasons, this requirement is recursive: all subobjects of the class (bases and non-static members) must also have trivial constructors.

Non trivial struct constructor inside a union in C++

The error messages you quoted are misleading, so no doubt you wonder why making the function non-virtual fixes the problem.

First - structs and classes in C++ have constructors and copy assignment operators. If you do not create them yourself, they will be created for you. (You may delete those default versions). In your example there is an automatically generated constructor of Data, and also automatically generated assignment operator.

Now why the error messages are misleading? Because they are not true. In C++ you can have union members with constructors or assignment operators. The problem starts when those member functions are not trivial. Before C++11 it was just not possible to have an union member with non-trivial constructor. C++11 changed it, but still without writing additional functions it is not possible to use such union.

A struct/class with virtual functions has non-trivial member functions as constructor and assignment operator (because hidden data member needs to be managed). This is why when you make the function non-virtual the errors disappear - then the member functions become trivial and your struct may be used as a union member without problems.

What does a non-trivial copy constructor do?


How does a non-trivial copy constructor differ from a trivial one due to the presence of a vptr?

The vptr is not copied from the source object, but has to be initialized to point to the virtual table of the destination class. Therefore, a straight "memcpy" copy from source to destination is not possible.

Also, keep in mind that having a vptr is not strictly a requirement, a compliant implementation could implement virtual dispatching some other way (I don't know what that would be though). Although, AFAIK, all implementations use this mechanism. But whichever way an implementation chooses to do things, it is clear that there will be some piece of information like a vptr that will have to be set in some way that is incompatible with a straight "memcpy" copy.

Why cannot the vptr be copied? If both objects of same type (same level in the inheritance), they both can point to the same vtable, can they not?

The sticky issue here is that assumption you made that "both objects of same type". This is not true in the general case. The source object could very well be of some other derived class and therefore have a different virtual table pointer. Another (more rare) practical issue has to do with cross-modular code (in different DLL/so files or executables), where two objects of the same type might use different virtual tables (e.g., there are some corner cases or hackish code in which this is possible without breaking ODR, like different template instantiations).

But the point is that there is no way that the compiler can be assured that for any use of the copy-constructor it is safe to just copy the vptr from the source object and expect it to be appropriate for the destination object.

Since I have defined my own copy constructor, does the compiler 'add' special instructions to my copy constructor to handle the virtualness?

Yes. It does. I don't remember the exact specification, but basically the requirement is that by the time you hit the body of the constructor, the vptr (or whatever other mechanism used for dynamic dispatch) is initialized. That essentially requires the compiler to add code to implicitly initialize the vptr, in all user-defined constructors.

Initializing a union with a non-trivial constructor

Question 1: Default constructors do initialize POD members to 0 according to the C++ standard. See the quoted text below.

Question 2: If a constructor must be specified in a base class, then that class cannot be part of a union.

Finally, you can provide a constructor for your union:

union U 
{
A a;
B b;

U() { memset( this, 0, sizeof( U ) ); }
};

For Q1:

From C++03, 12.1 Constructors, pg 190

The implicitly-defined default constructor performs the set of initializations of the
class that would be performed by a user-written default constructor for that class with an empty mem-initializer-list (12.6.2) and an empty function body.

From C++03, 8.5 Initializers, pg 145

To default-initialize an object of type T means:

  • if T is a non-POD class type
    (clause 9), the default constructor
    for T is called (and the
    initialization is ill-formed if T
    has no accessible default
    constructor);
  • if T is an array type, each element is default-initialized;
  • otherwise, the object is zero-initialized.

To zero-initialize an object of type T means:

  • if T is a scalar type (3.9), the object is set to the value of 0 (zero) converted to T;
  • if T is a non-union class type, each non static data member and each base-class subobject is zero-initialized;
  • if T is a union type, the object’s first named data member is zero-initialized;
  • if T is an array type, each element is zero-initialized;
  • if T is a reference type, no initialization is performed.

For Q2:

From C++03, 12.1 Constructors, pg 190

A constructor is trivial if it is an implicitly-declared default constructor and if:

  • its class has no virtual functions (10.3) and no virtual base classes (10.1), and
  • all the direct base classes of its class have trivial constructors, and
  • for all the nonstatic data members of its class that are of class type (or array
    thereof), each such class has a trivial constructor

From C++03, 9.5 Unions, pg 162

A union can have member functions (including constructors and destructors), but not virtual (10.3) functions. A union shall not have base classes. A union shall not be used as a base class.An object of a class with a non-trivial constructor (12.1), a non-trivial copy constructor (12.8), a non-trivial destructor (12.4), or a non-trivial copy assignment operator (13.5.3, 12.8) cannot be a member of a union, nor can an array of such objects

Non-triviality of class type due to presence of default member initializer(s)

All standard references below refers to N4659: March 2017 post-Kona working draft/C++17 DIS.


[class]/6 governs what is a trivial class [extract]:

[...] A trivial class is a class that is trivially copyable and has one or more default constructors, all of which are either trivial or deleted and at least one of which is not deleted. [...]

Let's denote the trivial class requirements as follows:

  • (A) the class is trivially copyable, and
  • (B) all the default constructors of the class are either trivial or deleted, and
  • (C) the class has at least one non-deleted default constructor.

As will be shown below, NonTrivial fulfills requirements (A) and (C), but fails requirement (B), and is thus not trivial.



Requirement (A): trivially copyable [fulfilled]

Requirement (A) is governed by [class]/6:

A trivially copyable class is a class:

  • (6.1) where each copy constructor, move constructor, copy assignment operator, and move assignment operator ([class.copy],
    [over.ass]) is either deleted or trivial,
  • (6.2) that has at least one non-deleted copy constructor, move constructor, copy assignment operator, or move assignment operator,
    and
  • (6.3) that has a trivial, non-deleted destructor.

where the first sub-requirement (6.1) covering triviality of constructors and assignment operators is governed by [class.copy.ctor]/11 and [class.copy.assign]/9, respectively:

[class.copy.ctor]/11

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

  • (11.1) class X has no virtual functions and no virtual base classes, and
  • (11.2) the constructor selected to copy/move each direct base class subobject is trivial, and
  • (11.3) 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.

[class.copy.assign]/9

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

  • (9.1) class X has no virtual functions and no virtual base classes, and
  • (9.2) the assignment operator selected to copy/move each direct base class subobject is trivial, and
  • (9.3) for each non-static data member of X that is of class type (or array thereof), the assignment operator selected to copy/move that
    member is trivial;

otherwise the copy/move assignment operator is non-trivial.

which are all fulfilled by NonTrivial.

The second sub-requirement (6.2) covering existence of constructors and assignment operators, when restricted to implicitly declared special functions (as is the case for this example), is governed by [class.copy.ctor]/6 and [class.copy.assign]/2, respectively:

[class.copy.ctor]/6

If the class definition does not explicitly declare a copy
constructor, a non-explicit one is declared implicitly. If the class
definition declares a move constructor or move assignment operator,
the implicitly declared copy constructor is defined as deleted;
otherwise, it is defined as defaulted. [...]

[class.copy.assign]/2

If the class definition does not explicitly declare a copy assignment
operator, one is declared implicitly. If the class definition
declares a move constructor or move assignment operator, the
implicitly declared copy assignment operator is defined as deleted;
otherwise, it is defined as defaulted. [...]

Thus, a copy constructor and copy assignment operator will be implicitly declared for NonTrivial. It follows from [class.copy.ctor]/8 and [class.copy.assign]/4 4that the same holds for for the move constructor and move assignment operator, respectively. Thus, NonTrivial fulfills the sub-requirement (6.2).

The third sub-requirement (6.3) is governed by [class.dtor]/6:

A destructor is trivial if it is not user-provided and if:

  • (6.1) the destructor is not virtual,
  • (6.2) all of the direct base classes of its class have trivial destructors, and
  • (6.3) for all of the non-static data members of its class that are of class type (or array thereof), each such class has a trivial
    destructor.

Otherwise, the destructor is non-trivial.

which is fulfilled by NonTrivial, and thus requirement (A) holds for NonTrivial.



Requirement (B): trivial or deleted default constructors [NOT fulfilled]

As governed by [class.ctor]/4 [extract]:

[...] If there is no user-declared constructor for class X, a non-explicit constructor having no parameters is implicitly declared as defaulted ([dcl.fct.def]).

a default constructor will be implicitly declared for the NonTrivial class. However, as governed by [class.ctor]/6, particularly [class.ctor]/6.2, this implicitly declared default constructor is not trivial:

A default constructor is trivial if it is not user-provided and if:

  • [...]
  • (6.2) no non-static data member of its class has a default member initializer ([class.mem]), and [...]

And thus the NonTrivial class fails the requirement (B), and is thus not trivial.

We may note that the clause [class.ctor]/6.2 originated from the original proposal for non-static data member initializers, N2628, which proposed adding

no non-static data member of its class has an assignment-initializer, and

as an additional triviality requirement to [class.ctor].



Requirement (C): existence of a default constructor [fulfilled]

For completeness, we may note that the NonTrivial class fulfills requirement (C), as per [class.ctor]/4 (quoted to in the section above).

Problem with union containing a glm::vec2 (non-trivial default constructor)


At most one non-static data member of a union may have a brace-or-equal-initializer. [Note: if any non-static data member of a union has a non-trivial default constructor (12.1), copy constructor (12.8), move constructor (12.8), copy assignment operator (12.8), move assignment operator (12.8), or destructor (12.4), the corresponding member function of the union must be user-provided or it will be implicitly deleted (8.4.3) for the union. — end note ]

The relevant part is

if any non-static data member of a union has a non-trivial [member function] [...], [then] the corresponding member function of the union must be user-provided or it will be implicitly deleted.

So you will have to manually implement the constructor and destructor (and if you need them also the copy constructor, move constructor, copy assignment operator and move assignment operator).

The compiler can't provide a default constructor because there's no way it could know which of the union members should be initialized by the default constructor, or which union member the destructor needs to destruct.

So using the default constructor or trying to =default; it won't work, you'll have to provide user-specified implementations.


A basic implementation could look like this:

struct Foo
{
bool isVector;
union
{
std::vector<float> vec;
struct { float width, height; };
};

// by default we construct a vector in the union
Foo() : vec(), isVector(true) {

}

void setStruct(float width, float height) {
if(isVector) {
vec.~vector();
isVector = false;
}
this->width = width;
this->height = height;
}

void setVector(std::vector<float> vec) {
if(!isVector) {
new (&this->vec) std::vector<float>();
isVector = true;
}

this->vec = vec;
}

~Foo() {
if(isVector) vec.~vector();
}
};

godbolt example

Note that you need to manually manage the lifetime of the union members, since the compiler won't do that for you.

In case the active union member is the struct, but you want to activate the vector member, you need to use placement new to construct a new vector in the union.

The same is true in the opposite direction: if you want to switch the active union member from the vector to the struct, you need to call the vector destructor first.

We don't need to clean up the struct, since it's trivial, so no need to call it's constructor / destructor.


If possible i would recommend using std::variant or boost::variant instead, since those do exactly that: they keep track of the active union member and call the required constructors / destructors as needed.

C++ Default constructors in union with variant member with non-trivial default constructor

cppreference is generaly a nice information source, but here the compiler is right. Draft n3337 for C++11 contains in 9.5 Unions [class.union] a non normative but explicit note saying:

If any non-static data member of a union has a non-trivial default
constructor (12.1), copy constructor (12.8), move constructor (12.8), copy assignment operator (12.8), move
assignment operator (12.8), or destructor (12.4), the corresponding member function of the union must be
user-provided or it will be implicitly deleted (8.4.3) for the union.

There is no mention that is will not be the case is one member has a default member initializer.

The same note still stands in draft n4296 for C++14 so I assume it should still be the same in the actual C++ standard. Notes are of course non normative, but their intent is to make more clear an interpretation of the standard, so I assume that the interpretation of cppreference is wrong because it does not respect that note, and that gcc interpretation is correct.

Why are C++ classes with virtual functions required to have a non-trivial copy constructor?


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.

What is a non-trivial destructor in C++?

You are getting your words mixed up. Your example does indeed declare an explicit destructor. You just forget to define it, too, so you'll get a linker error.

The rule is very straight-forward: Does your class have an explicit destructor? If yes, you're non-trivial. If no, check each non-static member object; if any of them are non-trivial, then you're non-trivial.

Union member has a non-trivial copy constructor

You cannot.

A union combines two pieces of functionality: the ability to store an object which may be of a select number of types, and the ability to effectively (and implementation-defined) convert between those types. You could put an integer in and look at its representation as a double. And so forth.

Because a union must support both of these pieces of functionality (and for a few other reasons, like being able to construct one), a union prevents you from doing certain things. Namely, you cannot put "live" objects in them. Any object that is "living" enough that it needs a non-default copy constructor (among many other restrictions) cannot be a member of a union.

After all, a union object does not really have the concept of which type of data it actually stores. It does not store one type of data; it stores all of them, at the same time. It is up to you to be able to fish the right type out. So how could it reasonably copy one union value into another?

Members of a union must be a POD (plain-old-data) type. And while C++11 does loosen those rules, objects still must have a default (or otherwise trivial) copy constructor. And std::string's copy constructor is non-trivial.

What you likely want is a boost::variant. That is an object that can store a number of possible types, just like a union. Unlike a union however, it is type-safe. It therefore knows what is actually in the union; it is therefore able to copy itself and otherwise behave like a regular C++ object.



Related Topics



Leave a reply



Submit