8
votes

I've been reading the C++ standard trying to understand if there are any observable differences between trivial, simple, and implicitly defined constructors/assignment operators/destructors. From my current understanding there doesn't seem to be a difference, but that seems odd, why spend so much time defining them when it doesn't matter?

As a particular concrete example, consider copy constructors.

  • A trivial copy constructor copies all fields and base classes field-by-field if all fields and base classes are trivial.
  • Otherwise, the implicitly generated copy constructor: "performs full member-wise copies of bases and non-static members in initialization order".

If I understand it correctly, if a class has all trivial bases and fields but has a defaulted copy-constructor, then the defaulted copy-constructor will do exactly the same thing as the trivial constructor. Not even the initialization order seems to be relevant here because the fields are all disjoint (since trivial implies the absence of virtual base classes).

Is there ever an instance when a trivial copy-constructor will do something different than an explicitly defaulted copy constructor?

Generally, the same logic seems to hold for other constructors and destructors as well. The argument for assignment is a little bit more complex due to the potential for data races, but it seems like all of those would be undefined behavior by the standard if the class was actually trivial.

2

2 Answers

6
votes

Not exactly about the behavior of the actual special member function per-se*, but consider the following:

struct Normal
{
    int a;
};

static_assert(std::is_trivially_move_constructible_v<Normal>);
static_assert(std::is_trivially_copy_constructible_v<Normal>);
static_assert(std::is_copy_constructible_v<Normal>);

This all seems well and good.

Now consider the following:

struct Strange
{
    Strange() = default;
    Strange(Strange&&) = default; 
};

static_assert(std::is_trivially_move_constructible_v<Strange>);
static_assert(!std::is_trivially_copy_constructible_v<Strange>);
static_assert(!std::is_copy_constructible_v<Strange>);

Hmm. The mere act of explicitly defaulting a move constructor disallows the object from being copy constructible!

Why is this?

Because, even though the compiler is still defining the move constructor for Strange, it's still a user-declared move constructor, which disables the generation of the copying special member functions.

The standard is very finicky about which special member functions get generated when you have user-declared versions of them, so it's best to stick with the Rule of Five or Zero.

Live Demo


Extra credit

By explicitly defaulting a default constructor for Strange, it is no longer an aggregate type (whereas Normal is). This opens up a whole different can of worms about initialization.


*Because as far as I know, the behavior of an explicitly defaulted special member function is identical to the trivial version of that function (or rather, it's the other way around). However, I have to note one peculiarity about the standard wording; when discussing the implicitly declared copy constructor, the standard neglects to say "implicitly declared as defaulted" like it does for the default and move constructors. I believe this to be a minor typo.

0
votes

As a particular concrete example, consider copy constructors.

  • A trivial copy constructor copies all fields and base classes field-by-field if all fields and base classes are trivial.
  • Otherwise, the implicitly generated copy constructor: "performs full member-wise copies of bases and non-static members in initialization order".

The Standard specifies the behavior of implicitly defined special functions in just one place each. For example, [class.copy.ctor]/11 defines whether or not a copy or move constructor qualifies as "trivial". [class.copy.ctor]/14, which contains the quote about "performs a memberwise copy/move", applies whether or not the copy or move constructor is "trivial". When paragraph 11 talks about "the constructor selected" to move a base or member, it's referring to the choices made by the (potential) definition described by paragraph 14.

So yes, being trivial doesn't make a difference for how the class object is initialized. Instead, it makes a difference for other uses of an object of that type, sometimes to allow treating the class type in a more "C-like" way. This isn't a complete listing, but some notable Standard rules which reference triviality:

  • Implicitly declared special member functions of a union or class containing an anonymous union are defined as deleted if the corresponding special member of any class-type variant member is not trivial.
  • It's valid to copy objects of a trivially copyable class (see [class.prop]/1) byte by byte. ([basic.types]/2-3).
  • It's always valid to pass an object of class type through a C-style variadic function's ... if the copy constructor, the move constructor (if any), and the destructor are all trivial. Otherwise, passing an object of the class type is conditionally supported. ([expr.call]/12)
  • Of course, the std::is_trivially_* traits can tell the difference.