Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Are there observable semantic differences between different implicitly generated functions?

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.

like image 639
Gregory Avatar asked Aug 27 '20 02:08

Gregory


1 Answers

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.

like image 52
AndyG Avatar answered Nov 05 '22 06:11

AndyG