As I understand the standard, a trivial destructor is one which is implicitly declared and whose class has only base and non-static members with trivial destructors.
Given the recursivity of this definition, it seems to me that the only "recursion-stopping" condition is to find a base or non-static member with a non-implicitly declared destructor (i.e. user declared).
If that's right, that should mean that a trivial destructor is one which "doesn't have to do anything" and hence it will be declared (implicitly) but not defined.
Saying it in another way: is it correct to say that an implicitly defined destructor (that is "it does something") cannot be trivial as per the standard definition?
Sorry for the kind of silly question, but I'd like to clarify things a bit in my head...
No. An implicitly defined, trivial destructor is by definition trivial :) The difference between the declare and define thingy is that in order for the compiler to even see that a destructor is available, there must always a declaration. So if you don't provide one, it will implicitly provide one.
But now, it will also define one, if that is needed (if an object of that class type is destroyed). In any case, it has to do something: It needs to call the destructors of all its members and base classes. A simple example which illustrates the effect of implicitly defining a destructor:
struct a {
private:
~a();
};
struct bug {
// note: can't be destructed
a a_;
};
As soon as you try to create a local object of bug, the compiler will signal an error, because it yields a definition of a destructor for bug, which tries to call the not accessible destructor of a.
Now, i think triviality of destructors/constructors are mostly used to put constraints on your program. Objects having non-trivial versions of them can't be put in unions, for example. On the other side, you can delete an object having incomplete type, provided it has a trivial destructor. Note that if your program can't decide whether or not a trivial destructor was actually defined, the compiler is allowed to omit defining it. That's the so-called as-if rule. The compiler has to behave as-if it's Standard compliant - optimizations do not matter as long as they don't change the meaning of a program.
Your wording is a bit unfortunate. E.g. the recursion of course also ends when you run out of members and base classes. Those wording problems also seem to get you more confused.
Anyway, all implicitly-declared destructors, whether they are trivial or not, are defined if and only if they are used. Used is a specific term here. A destructor of type T is used whenever the lifetime of a T object ends.
Trivial destructors exist because C programmers put structs in unions. This code should remian legal in C++, so the notion of a trivial destructor was invented for C++. All C structs have trivial destructors, when compiled as C++.
Consider these two classes:
class A {
};
class B {
private:
A obj;
};
The destructors of both these classes implicitly defined. Yet, at the same time, both of them are trivial by the standard definition.
Related
In an std::vector<T> the vector owns the allocated storage and it constructs Ts and destructs Ts. Regardless of T's class hierarchy, std::vector<T> knows that it has only created a T and thus when .pop_back() is called it only has to destroy a T (not some derived class of T). Take the following code:
#include <vector>
struct Bar {
virtual ~Bar() noexcept = default;
};
struct FooOpen : Bar {
int a;
};
struct FooFinal final : Bar {
int a;
};
void popEm(std::vector<FooOpen>& v) {
v.pop_back();
}
void popEm(std::vector<FooFinal>& v) {
v.pop_back();
}
https://godbolt.org/z/G5ceGe6rq
The PopEm for FooFinal simply just reduces the vector's size by 1 (element). This makes sense. But PopEm for FooOpen calls the virtual destructor that the class got by extending Bar. Given that FooOpen is not final, if a normal delete fooOpen was called on a FooOpen* pointer, it would need to do the virtual destructor, but in the case of std::vector it knows that it only made a FooOpen and no derived class of it was constructed. Therefore, couldn't std::vector<FooOpen> treat the class as final and omit the call to the virtual destructor on the pop_back()?
Long story short - compiler doesn't have enough context information to deduce it https://godbolt.org/z/roq7sYdvT
Boring part:
The results are similar for all 3: msvc, clang, and gcc, so I guess the problem is general.
I analysed the libstdc++ code just to find pop_back() runs like this:
void pop_back() // a bit more convoluted but boils-down to this
{
--back;
back->~T();
}
Not any surprise. It's like in C++ textbooks. But it shows the problem - virtual call to a destructor from a pointer.
What we're looking for is the 'devirtualisation' technique described here: Is final used for optimisation in C++ - it states devirtualisation is 'as-if' behaviour, so it looks like it is open for optimisation if the compiler has enough information to do it.
My opinion:
I meddled with the code a little and i think optimisation doesn't happen because the compiler cannot deduce the only objects pointed by "back" are FooOpen instances. We - humans - know it because we analyse the entire class, and see the overall concept of storing the elements in a vector. We know the pointer must point to FooOpen instance only, but compiler fails to see it - it only sees a pointer which can point anywhere (vector allocates uninitialized chunk of memory and its interpretation is a part of vector's logic, also the pointer is modified outside the scope of pop_back()). Without knowing the entire concept of vector<> i don't think of how it can be deduced (without analysing the entire class) that it won't point to any descendant of FooOpen which can be defined in other translation units.
FooFinal doesn't have this problem because it already guarantees no other class can inherit from it so devirtualisation is safe for objects pointed by FooFinal* or FooFinal&.
Update
I made several findings which may be useful:
https://godbolt.org/z/3a1bvax4o), devirtualisation can occur for non-final classes as long as there is no pointer arithmetic involved.
https://godbolt.org/z/xTdshfK7v std::array performs devirtualisation on non-final classes. std::vector fails to do it even if it is constructed and destroyed in the same scope.
https://godbolt.org/z/GvoaKc9Kz devirtualisation can be enabled using wrapper.
https://godbolt.org/z/bTosvG658 destructor devirtualisation can be enabled with allocator. Bit hacky, but is transparent to the user. Briefly tested.
Yes, this is a missed optimisation.
Remember that a compiler is a software project, where features have to be written to exist. It may be that the relative overhead of virtual destruction in cases like this is low enough that adding this in hasn't been a priority for the gcc team so far.
It is an open-source project, so you could submit a patch that adds this in.
It feels a lot like § 11.4.7 (14) gives some insight into this. As of latest working draft (N4910 Post-Winter 2022 C++ working draft, Mar. 2022):
After executing the body of the destructor and destroying any objects with automatic storage duration
allocated within the body, a destructor for class X calls the destructors for X’s direct non-variant non-static data
members, the destructors for X’s non-virtual direct base classes and, if X is the most derived class (11.9.3), its
destructor calls the destructors for X’s virtual base classes. All destructors are called as if they were referenced
with a qualified name, that is, ignoring any possible virtual overriding destructors in more derived classes.
Bases and members are destroyed in the reverse order of the completion of their constructor (see 11.9.3).
[Note 4 : A return statement (8.7.4) in a destructor might not directly return to the caller; before transferring control
to the caller, the destructors for the members and bases are called. — end note]
Destructors for elements of an array are called in reverse order of their construction (see 11.9).
Also interesting for this topic, § 11.4.6, (17):
In an explicit destructor call, the destructor is specified by a ~ followed by a type-name or decltype-specifier
that denotes the destructor’s class type. The invocation of a destructor is subject to the usual rules for
member functions (11.4.2); that is, if the object is not of the destructor’s class type and not of a class derived
from the destructor’s class type (including when the destructor is invoked via a null pointer value), the program has undefined behavior.
So, as far as the standard cares, the invocation of a destructor is subject to the usual rules for member functions.
This, to me, sounds a lot like destructor calls do so much that compilers are likely unable to determine, at compile-time, that a destructor call does "nothing" - as it also calls destructors of members, and std::vector doesn't know this.
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.
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.
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.
Although using =default for constructors is clear for me (i.e. forcing the compiler to create the default constructor while other ctors exist), I still cannot understand the difference between these two type of destructors:
Those that use =default
Those that are not defined explicitly and generated by compiler automatically.
The only thing that comes to my mind is that the group-1 destructors can be defined as virtual, but group-2 is always non-virtual. So, is that the only difference between them? Is there any scenarios that the compiler is not generating the destructor, but using =default forces the compiler to generate it?
p.s. I have checked lots of Qs in stackoverflow, but none of them answers my Q. Here are some relevant questions.
Difference between =default and {} ctos/destructors
Defaulting virtual destructors
Difference between =default and empty dtrs
Edit 1: This Q on SO, focuses on disabling the default move constructors, that can be considered as ONE of the items mentioned in the accepted answer.
(Several of the points below were already mentioned in comments or in the linked questions; this answer serves to organize and interrelate them.)
There are of course three ways to get a “simple destructor”:
struct Implicit {};
struct Empty {~Empty() {}};
struct Defaulted {~Defaulted()=default;};
Like a default (and not a copy or move) constructor, {} and =default; mean largely the same thing for destructors. The interesting properties of Defaulted are then those (combinations) that differ from both of the others.
Versus Empty the main difference is simple: the explicitly defaulted destructor can be trivial. This applies only if it is defaulted inside the class, so there is no difference between {} and =default; on an out-of-line definition. Similarly, being virtual removes any distinction, as does having any member or base class with a non-trivial destructor. There is also the distinction that an explicitly defaulted destructor can be implicitly defined as deleted. Both of these properties are shared with implicitly declared destructors, so we have to find a distinction from those as well.
Versus Implicit, an explicitly defaulted destructor suppresses move operations, can be declared private, protected, or noexcept(false), and in C++20 can be constrained (but not consteval). Very marginally, it can be declared constexpr to verify that it would be anyway. Declaring it inline doesn’t do anything. (It can also be out of line or virtual, but as stated above that can’t be a reason to use it.)
So the answer is “when you want a trivial (or potentially deleted) destructor that has other special properties”—most usefully, access control or noexcept status.
I am reading textbook "C++ Primer Plus, Prata"
A paragraph in chapter 10 catches my eye and confuses me.
Ch.10 Objects and classes:
It says
If you don't provide one, the compiler implicitly declares a default constructor and,...
I thought it should be
If you don't provide destructor, the compiler implicitly declares a default destructor and,...
Is the paragraph correct?
How should I explain that correctly?
Thank you
The "one" part is correct. That's just a nuance of English grammar where you can refer to something in a dependent clause that comes later in the sentence. Think of it like a forward declaration! The "default constructor" part is actually a typo: it should be "default destructor", like you originally thought.
It should say this:
Because a destructor is called automatically when a class object expires, there ought to be a destructor. If you don't provide one [a destructor], the compiler implicitly declares a default destructor and, if it detects code that leads to the destruction of an object, it provides a definition for the destructor.
Here, "one" refers to "a destructor," which comes later in the sentence. Another key to understanding the sentence is keeping in mind the distinction between declaring a function and defining a function. The compiler always declares an implicit destructor if you don't provide one, but it only defines it if it needs it (that is, if that destructor is going to be called).
What makes it all the more confusing (and probably what led to the typo) is that all of this is equally true for constructors.
Let's see if we can improve on the paragraph:
Because the destructor will be called automatically when a class object goes out of scope, all classes must have a destructor. If you don't explicitly provide one, the compiler implicitly declares a default destructor. If the compiler detects code that leads to the destruction of an object, it also provides a default definition for the destructor. The same thing is true for constructors.
I actually got the idea of this question when I was discussing on another question of mine (Member not zeroed, a clang++ bug?). That question is about C++11 value-initialization, but when I saw the C++03 value-initialization rule someone posted there, I am confused.
The value-initialization rule from C++03 is:
To value-initialize an object of type T means:
if T is a class type (clause 9) with a user-declared constructor (12.1), then the default constructor for T is called (and the
initialization is ill-formed if T has no accessible default
constructor);
if T is a non-union class type without a user-declared constructor, then every non-static data member and base-class component of T is
value-initialized;
if T is an array type, then each element is value-initialized;
otherwise, the object is zero-initialized
Please look at the second bullet which defines the value-initialize process for a type without user-declared constructor. This rule doesn't mention a constructor call. As we can see from the description of other cases of value-initialize or from the description of default-initialize, if constructor should be called, it will be explicitly mentioned in the text of the standard. I know there is certain initialization form that constructor doesn't get called (e.g. {}-initialization for aggregates), but should it be the case for value-initialize of non-union class type without a user-declared constructor? The implicitly declared constructor of such a type could easily be non-trivial. For example:
class A {
public:
virtual void f() {}
};
According to the rule in C++03, if the implicitly declared non-trivial constructor doesn't get called in the process of the value-initialization of an object of A, how does the vptr of the object get setup? (I know things related to vptr is all implementation-defined, but this doesn't change the major point I'm trying to make here.)
(Someone would argue that the absence of mentioning a constructor call in the rule doesn't mean constructor won't get called. OK. Let's say constructor will get called according to some other rule I may have overlooked, but since all members need to be value-initialized anyway, wouldn't that cause the members' constructors to be called more than once?)
Asking a question for C++03 when it's already C++11 everywhere may seem worthless. Yeah, that's a valid point. However, I think I could more or less learn something if I finally figure this out (whether I am wrong and why).
EDIT: Maybe I shouldn't have used vptr as an example. My point is, wouldn't skipping the call to a non-trivial constructor cause some potential problem for the validity of the object? After all, it's called non-trivial for a reason.
As far as the Standard is concerned, the constructor is not responsible for setting up the vtable. There is nothing responsible for setting up the vtable; vtables don't exist, as far as the Standard is concerned.
Rather, the vtable is a consequence of the other rules the compiler has to follow, relating to virtual function binding and such. So whether or not the constructor is called, the vtable's going to be set up somewhere, because otherwise the compiler will have trouble meeting its other obligations. That doesn't contradict the value-initialization rule; rather, it adds nuance to the practicalities of implementing the rule.
When a class X has no user-provided constructor, its default constructor does exactly what a constructor of the form X::X() {} would (C++03[class.ctor]§6). And as far as the standard is concerned, this is defined to perform default initialisation of all members and base class subobjects, and nothing else. So "calling the generated default constructor" is identical to "default-initialising all data members and base class subobjects."
So this actually does "less" than the value initialisation you quoted - as that value initialisation value-initialises all data members and base class subobjects. So it does all the constructor does, and more.
As far as implementation-specific things (like the vtable pointer) go, these are outside of the scope of the standard. It is a compiler's responsibility to make sure all of its implementation-specific mechanisms work regardless of constructor calls mandated by the standard.