Is there no such thing as "implicit this parameter" in the Standard? - c++

Recently, I asked this question where one of the answers says:
There's no such thing as "implicit this parameter" in the standard. The standard calls it an "implicit object parameter".
Then someone commented that:
There's no such thing as "implicit this parameter" in the standard." seems wrong. From expr.call#4: "If the function is a non-static member function, the this parameter of the function shall be initialized with a pointer to the object of the call, converted as if by an explicit type conversion."
Seeing the above comment i think that the answer is technically incorrect because the answer said that "There's no such thing as "implicit this parameter" in the standard." while the standard clearly talks about the this parameter.
So how to interpret this further (pun intended)? I mean, it seems that the standard makes a distinction between the non-static member function and a constructor in the context of this parameter. For example, the standard says that for a non-static member function, the this parameter of the function shall be initialized with a pointer to the object of the call converted as if by an explicit type conversion. But the standard doesn't say the same for constructors. So why does the standard makes this distinction? I mean why doesn't the standard says that constructors also have an this parameter that get initialized by the passed argument just like for non-static member functions. This again leads to the deeper question that if there is no this parameter in the constructor unlike non-static member function, then how are we able to use this inside the constructor. For example, we know that we can write this->p = 0 inside the constructor as well as inside a non-static member function, where p is a data member. But in case of non-static member function, this is a parameter of that particular member function so this->p makes sense. But in case of constructor this is not a parameter, so how are we able to use this->p inside the constructor.
Originally, by reading the answers here, I thought that the implicit this parameter is an implementation detail. But after reading expr.call#4 it seems that it is not an implementation detail.

If you think this is some sort of implicit parameter, type in this code:
#include <iostream>
struct SimpleThing {
int xyzzy;
SimpleThing(): xyzzy(42) {}
void print(int plugh, const int twisty) {
std::cout << xyzzy << '\n';
std::cout << plugh << '\n';
std::cout << twisty << '\n';
xyzzy = 0;
plugh = 0;
twisty = 0;
this = 0;
}
};
int main() {
SimpleThing thing;
thing.print(7, 99);
}
Then examine the errors you get:
prog.cpp: In member function ‘void SimpleThing::print(int, int)’:
prog.cpp:12:16: error: assignment of read-only parameter ‘twisty’
12 | twisty = 0;
| ~~~~~~~^~~
prog.cpp:13:16: error: lvalue required as left operand of assignment
13 | this = 0;
| ^
Note that the first two assignments work because they are modifiable variables. The third fails because it is, of course, (non-modifiable) const.
The attempted assignment to this doesn't look like any sort of "can't write to some sort of variable" diagnostic because it actually isn't.
The this keyword is a special marker inside non-static member functions (and constructors/destructors) that is translated into the address of the object being worked upon. While it may be passed as a hidden parameter, that is very much an implementation detail with which the standard does not concern itself.
The controlling section in the C++20 standard is in [class.this]:
In the body of a non-static member function, the keyword this is a prvalue whose value is the address of the object for which the function is called.
Nowhere in there (the entire section) does it mention that this is some sort of hidden parameter to the call.
And, regarding your question on why there is a distinction between non-static member functions and constructors, I don't believe this distinction involves the existence of this in either case, it instead has to do with the qualification of the type of this. It's existence in a constructor is undeniable as [class.ctor] states:
During the construction of an object, if the value of the object or any of its subobjects is accessed through a glvalue that is not obtained, directly or indirectly, from the constructor’s this pointer, the value of the object or subobject thus obtained is unspecified.
In other words, I see your quote:
If the function is a non-static member function, the this parameter of the function is initialized with a pointer to the object of the call, converted as if by an explicit type conversion.
as specifying only the qualification of this, something that the constructor doesn't need.
There is no discussion of cv-qualified conversion for constructors as there is for other member functions because you can't actually create a cv-qualified constructor. It would be rather useless if your constructor were not allowed to set any member variables, for example :-)
While constructors can be used to create cv-qualified objects, the constructor itself is not cv-qualified. This is covered at the end of [class.this]:
Constructors and destructors shall not be declared const, volatile or const volatile. [Note: However, these functions can be invoked to create and destroy objects with cv-qualified types - end note]
And further in [class.ctor]:
A constructor can be invoked for a const, volatile or const volatile object. Const and volatile semantics are not applied on an object under construction. They come into effect when the
constructor for the most derived object ends.
To be honest, I think WG21 would be better off going through the next iteration and replacing things like "the this parameter of the function" with a phrase that does not mention parameters at all (such as "the this property".

Here's a quotation from this Draft C++17 Standard (bolding for emphasis, and to answer the question, is mine):
10.3.3 The using declaration      [namespace.udecl]
…
16    
For the purpose of forming a set of candidates during overload
resolution, the functions that are introduced by a using-declaration
into a derived class are treated as though they were members of the
derived class. In particular, the implicit this parameter shall
be treated as if it were a pointer to the derived class rather than to
the base class. This has no effect on the type of the function, and in
all other respects the function remains a member of the base class.
Likewise, constructors that are introduced by a using-declaration
are treated as though they were constructors of the derived class when
looking up the constructors of the derived class …
However I should add that the cited paragraph doesn't seem to be present in this later Draft Standard. In fact, that (later) Standard seems to use the phrase, "implicit object parameter," in similar clauses.
So, maybe you should add a specific version tag to your question: c++17 or c++20, as there appears to be a divergence in the use (or not) of the term.
Note that the above citation is the only occurrence of the phrase, "implicit this parameter" in that Draft Standard.
Also, note that both documents I have linked are only Draft versions of the respective Standards, and both come with this cautionary escape-clause:
Note: this is an early draft. It’s known to be incomplet and
incorrekt, and it has lots of bad formatting.

Related

Use specific std::get overload in a function [duplicate]

Consider the following code:
#include <cctype>
#include <functional>
#include <iostream>
int main()
{
std::invoke(std::boolalpha, std::cout); // #1
using ctype_func = int(*)(int);
char c = std::invoke(static_cast<ctype_func>(std::tolower), 'A'); // #2
std::cout << c << "\n";
}
Here, the two calls to std::invoke are labeled for future reference.
The expected output is:
a
Is the expected output guaranteed in C++20?
(Note: there are two functions called tolower — one in <cctype> and the other in <locale>. The explicit cast is introduced to select the desired overload.)
Short answer
No.
Explanation
[namespace.std] says:
Let F denote a standard library function ([global.functions]), a standard library static member function, or an instantiation of a standard library function template.
Unless F is designated an addressable function, the behavior of a C++ program is unspecified (possibly ill-formed) if it explicitly or implicitly attempts to form a pointer to F.
[Note: Possible means of forming such pointers include application of the unary & operator ([expr.unary.op]), addressof ([specialized.addressof]), or a function-to-pointer standard conversion ([conv.func]).
— end note ]
Moreover, the behavior of a C++ program is unspecified (possibly ill-formed) if it attempts to form a reference to F or if it attempts to form a pointer-to-member designating either a standard library non-static member function ([member.functions]) or an instantiation of a standard library member function template.
With this in mind, let's check the two calls to std::invoke.
The first call
std::invoke(std::boolalpha, std::cout);
Here, we are attempting to form a pointer to std::boolalpha. Fortunately, [fmtflags.manip] saves the day:
Each function specified in this subclause is a designated addressable function ([namespace.std]).
And boolalpha is a function specified in this subclause.
Thus, this line is well-formed, and is equivalent to:
std::cout.setf(std::ios_base::boolalpha);
But why is that? Well, it is necessary for the following code:
std::cout << std::boolalpha;
The second call
std::cout << std::invoke(static_cast<ctype_func>(std::tolower), 'A') << "\n";
Unfortunately, [cctype.syn] says:
The contents and meaning of the header <cctype> are the same as the C standard library header <ctype.h>.
Nowhere is tolower explicitly designated an addressable function.
Therefore, the behavior of this C++ program is unspecified (possibly ill-formed), because it attempts to form a pointer to tolower, which is not designated an addressable function.
Conclusion
The expected output is not guaranteed.
In fact, the code is not even guaranteed to compile.
This also applies to member functions.
[namespace.std] doesn’t explicitly mention this, but it can be seen from [member.functions] that the behavior of a C++ program is unspecified (possibly ill-formed) if it attempts to take the address of a member function declared in the C++ standard library. Per [member.functions]/2:
For a non-virtual member function described in the C++ standard library, an implementation may declare a different set of member function signatures, provided that any call to the member function that would select an overload from the set of declarations described in this document behaves as if that overload were selected. [ Note: For instance, an implementation may add parameters with default values, or replace a member function with default arguments with two or more member functions with equivalent behavior, or add additional signatures for a member function name. — end note ]
And [expr.unary.op]/6:
The address of an overloaded function can be taken only in a context that uniquely determines which version of the overloaded function is referred to (see [over.over]). [ Note: Since the context might determine whether the operand is a static or non-static member function, the context can also affect whether the expression has type “pointer to function” or “pointer to member function”. — end note ]
Therefore, the behavior of a program is unspecified (possibly ill-formed) if it explicitly or implicitly attempts to form a pointer to a member function in the C++ library.
(Thanks for the comment for pointing this out!)

C++ type deduction of overloaded function [duplicate]

Consider the following code:
#include <cctype>
#include <functional>
#include <iostream>
int main()
{
std::invoke(std::boolalpha, std::cout); // #1
using ctype_func = int(*)(int);
char c = std::invoke(static_cast<ctype_func>(std::tolower), 'A'); // #2
std::cout << c << "\n";
}
Here, the two calls to std::invoke are labeled for future reference.
The expected output is:
a
Is the expected output guaranteed in C++20?
(Note: there are two functions called tolower — one in <cctype> and the other in <locale>. The explicit cast is introduced to select the desired overload.)
Short answer
No.
Explanation
[namespace.std] says:
Let F denote a standard library function ([global.functions]), a standard library static member function, or an instantiation of a standard library function template.
Unless F is designated an addressable function, the behavior of a C++ program is unspecified (possibly ill-formed) if it explicitly or implicitly attempts to form a pointer to F.
[Note: Possible means of forming such pointers include application of the unary & operator ([expr.unary.op]), addressof ([specialized.addressof]), or a function-to-pointer standard conversion ([conv.func]).
— end note ]
Moreover, the behavior of a C++ program is unspecified (possibly ill-formed) if it attempts to form a reference to F or if it attempts to form a pointer-to-member designating either a standard library non-static member function ([member.functions]) or an instantiation of a standard library member function template.
With this in mind, let's check the two calls to std::invoke.
The first call
std::invoke(std::boolalpha, std::cout);
Here, we are attempting to form a pointer to std::boolalpha. Fortunately, [fmtflags.manip] saves the day:
Each function specified in this subclause is a designated addressable function ([namespace.std]).
And boolalpha is a function specified in this subclause.
Thus, this line is well-formed, and is equivalent to:
std::cout.setf(std::ios_base::boolalpha);
But why is that? Well, it is necessary for the following code:
std::cout << std::boolalpha;
The second call
std::cout << std::invoke(static_cast<ctype_func>(std::tolower), 'A') << "\n";
Unfortunately, [cctype.syn] says:
The contents and meaning of the header <cctype> are the same as the C standard library header <ctype.h>.
Nowhere is tolower explicitly designated an addressable function.
Therefore, the behavior of this C++ program is unspecified (possibly ill-formed), because it attempts to form a pointer to tolower, which is not designated an addressable function.
Conclusion
The expected output is not guaranteed.
In fact, the code is not even guaranteed to compile.
This also applies to member functions.
[namespace.std] doesn’t explicitly mention this, but it can be seen from [member.functions] that the behavior of a C++ program is unspecified (possibly ill-formed) if it attempts to take the address of a member function declared in the C++ standard library. Per [member.functions]/2:
For a non-virtual member function described in the C++ standard library, an implementation may declare a different set of member function signatures, provided that any call to the member function that would select an overload from the set of declarations described in this document behaves as if that overload were selected. [ Note: For instance, an implementation may add parameters with default values, or replace a member function with default arguments with two or more member functions with equivalent behavior, or add additional signatures for a member function name. — end note ]
And [expr.unary.op]/6:
The address of an overloaded function can be taken only in a context that uniquely determines which version of the overloaded function is referred to (see [over.over]). [ Note: Since the context might determine whether the operand is a static or non-static member function, the context can also affect whether the expression has type “pointer to function” or “pointer to member function”. — end note ]
Therefore, the behavior of a program is unspecified (possibly ill-formed) if it explicitly or implicitly attempts to form a pointer to a member function in the C++ library.
(Thanks for the comment for pointing this out!)

Why the sentence "The expression can be used only as the left-hand operand of a member function call" in [expr.ref]p(6.3.2)?

[expr.ref]p(6.3.2):
Otherwise, if E1.E2 refers to a non-static member function and the
type of E2 is “function of parameter-type-list cv
ref-qualifieropt returning T”, then E1.E2 is a prvalue. The expression designates a non-static member function. The
expression can be used only as the left-hand operand of a member
function call ([class.mfct]). [ Note: Any redundant set of
parentheses surrounding the expression is ignored ([expr.prim.paren]).
— end note ] The type of E1.E2 is “function of parameter-type-list
cv returning T”.
For example the second statement in main below doesn't compile, probably because of the highlighted sentence above. But why is the language set up to work this way?
#include<iostream>
void g();
struct S { void f(); };
S s;
int main(){
std::cout << "decltype(g) == void() ? " << std::is_same<decltype(g), void()>::value << '\n'; // Ok
std::cout << "decltype(s.f) == void() ? " << std::is_same<decltype(s.f), void()>::value << '\n'; // Doesn't compile probably because of the sentence hihlighted above in [expr.ref]p(6.3.2).
}
When you do E1.E2, you are not talking about a general property of the type of thing that E1 is. You're asking to access a thing within the object designated by E1, where the name of the thing to be accessed is E2. If E2 is static, it accesses the class static thing; if E2 is non-static, then it accesses the member thing specific to that object. That's important.
Member variables become the subobject. If your class S had a non-static data member int i;, s.i is a reference to an int. That reference, from the stand point of an int&, behaves no differently from any other int&.
Let me say that more clearly: any int* or int& can point to/reference an int which is a complete object or an int which is a subobject of some other object. The single construct int& can serve double-duty in this way.*
Given that understanding of s.i, what would be the presumed meaning of s.f? Well, it should be similar, right? s.f would be some kind of thing that, when called with params, will be the equivalent of doing s.f(params).
But that is not a thing which exists in C++.
There is no language construct in C++ which can represent that meaning of s.f. Such a construct would need to store a reference to s as well as the member S::f.
A function pointer can't do that. Function pointers need to be able to be pointer-interconvertible with void***. But such an s.f would need to store the member S::f as well as a reference to s itself. So by definition, it'll have to be bigger than a void*.
A member pointer can't do that either. Member pointers explicitly don't carry their this object along with them (that's kind of the point); you must provide them at call-time using the specific member pointer call syntax .* or .->.
Oh, there are ways to encode this within the language: lambdas, std::bind, etc. But there is no language-level construct which has this precise meaning.
Because C++ is asymmetric in this way, where s.i has an encodable meaning but not s.f, C++ makes the unencodable one illegal.
You may ask why such a construct doesn't simply get built. It's not really that important. The language works perfectly fine as is, and due to the complexity of what an s.f would need to be, it's probably best to make you use a lambda (for which admittedly there should be ways to make it shorter to write such things) if that's what you want.
And if you want a naked s.f to be equivalent to S::f (ie: designates the member function), that doesn't really work either. First, S::f doesn't have a type either; the only thing you can do with such a prvalue is convert it to a pointer to a member. Second, a member function pointer doesn't know what object it came from, so in order to use one to call the member, you need to give it s. Therefore, in a call expression, s would have to appear twice. Which is really silly.
*: there are things you can do to complete objects that you cannot do to subobjects. But those provoke UB because they're not detectable by the compiler, because an int* doesn't say if it comes from a subobject or not. Which is the main point; nobody can tell the difference.
**: the standard does not require this, but the standard cannot do something which out-right makes such an implementation impossible. Most implementations provide this functionality, and basically any DLL/SO loading code relies on it. Oh, and it would also be completely incompatible with C, which makes it a non-starter.
The stat_result.st_mtime syntax was inherited from C, where it always has a value (more particularly, an lvalue). It is therefore syntactically an expression even in the case of a method call, but it has no value because it is evaluated in concert with its associated call expression.
It is (as you quoted) given the type of the member function so as to satisfy the requirement that a function be called via an expression of the correct type. It would, however, be misleading to define decltype for it since it cannot be a full-expression (as is every unevaluated operand) and common expression SFINAE with decltype would not prevent a hard error from the guarded instantiation.
Note that a non-static member function can be named in an unevaluated operand, but that allows S::f (or just f within the class, although that is arguably rewritten to be (*this).f in a member function), not s.f. That expression has the same type but is itself restricted (to appearing with & to form a pointer to member [function]) for the same reason: if it were to be used otherwise, it would be usable as a normal [function] pointer, which is impossible.

Can I take the address of a function defined in standard library?

Consider the following code:
#include <cctype>
#include <functional>
#include <iostream>
int main()
{
std::invoke(std::boolalpha, std::cout); // #1
using ctype_func = int(*)(int);
char c = std::invoke(static_cast<ctype_func>(std::tolower), 'A'); // #2
std::cout << c << "\n";
}
Here, the two calls to std::invoke are labeled for future reference.
The expected output is:
a
Is the expected output guaranteed in C++20?
(Note: there are two functions called tolower — one in <cctype> and the other in <locale>. The explicit cast is introduced to select the desired overload.)
Short answer
No.
Explanation
[namespace.std] says:
Let F denote a standard library function ([global.functions]), a standard library static member function, or an instantiation of a standard library function template.
Unless F is designated an addressable function, the behavior of a C++ program is unspecified (possibly ill-formed) if it explicitly or implicitly attempts to form a pointer to F.
[Note: Possible means of forming such pointers include application of the unary & operator ([expr.unary.op]), addressof ([specialized.addressof]), or a function-to-pointer standard conversion ([conv.func]).
— end note ]
Moreover, the behavior of a C++ program is unspecified (possibly ill-formed) if it attempts to form a reference to F or if it attempts to form a pointer-to-member designating either a standard library non-static member function ([member.functions]) or an instantiation of a standard library member function template.
With this in mind, let's check the two calls to std::invoke.
The first call
std::invoke(std::boolalpha, std::cout);
Here, we are attempting to form a pointer to std::boolalpha. Fortunately, [fmtflags.manip] saves the day:
Each function specified in this subclause is a designated addressable function ([namespace.std]).
And boolalpha is a function specified in this subclause.
Thus, this line is well-formed, and is equivalent to:
std::cout.setf(std::ios_base::boolalpha);
But why is that? Well, it is necessary for the following code:
std::cout << std::boolalpha;
The second call
std::cout << std::invoke(static_cast<ctype_func>(std::tolower), 'A') << "\n";
Unfortunately, [cctype.syn] says:
The contents and meaning of the header <cctype> are the same as the C standard library header <ctype.h>.
Nowhere is tolower explicitly designated an addressable function.
Therefore, the behavior of this C++ program is unspecified (possibly ill-formed), because it attempts to form a pointer to tolower, which is not designated an addressable function.
Conclusion
The expected output is not guaranteed.
In fact, the code is not even guaranteed to compile.
This also applies to member functions.
[namespace.std] doesn’t explicitly mention this, but it can be seen from [member.functions] that the behavior of a C++ program is unspecified (possibly ill-formed) if it attempts to take the address of a member function declared in the C++ standard library. Per [member.functions]/2:
For a non-virtual member function described in the C++ standard library, an implementation may declare a different set of member function signatures, provided that any call to the member function that would select an overload from the set of declarations described in this document behaves as if that overload were selected. [ Note: For instance, an implementation may add parameters with default values, or replace a member function with default arguments with two or more member functions with equivalent behavior, or add additional signatures for a member function name. — end note ]
And [expr.unary.op]/6:
The address of an overloaded function can be taken only in a context that uniquely determines which version of the overloaded function is referred to (see [over.over]). [ Note: Since the context might determine whether the operand is a static or non-static member function, the context can also affect whether the expression has type “pointer to function” or “pointer to member function”. — end note ]
Therefore, the behavior of a program is unspecified (possibly ill-formed) if it explicitly or implicitly attempts to form a pointer to a member function in the C++ library.
(Thanks for the comment for pointing this out!)

Why can't a destructor be marked constexpr?

In C++, you can declare many things as constexpr: variables, functions (including member functions and operators), constructors, and since C++1z, also if statements and lambda expressions. However, declaring a destructor constexpr results in an error:
struct X {
constexpr ~X() = default; // error: a destructor cannot be 'constexpr'
};
My questions:
Why can't a destructor be marked constexpr?
If I do not provide a destructor, is the implicitly generated destructor constexpr?
If I declare a defaulted destructor (~X() = default;), is it automatically constexpr?
As per the draft basic.types#10 possibly cv-qualified class type that has all of the following properties:
A possibly cv-qualified class type that has all of the following properties:
(10.5.1) - it has a trivial destructor,
(10.5.2) - it is either a closure type, an aggregate type, or has at
least one constexpr constructor or constructor template (possibly
inherited from a base class) that is not a copy or move constructor,
(10.5.3) - if it is a union, at least one of its non-static data
members is of non-volatile literal type
(10.5.4) - if it is not
a union, all of its non-static data members and base classes are of
non-volatile literal types.
Ques 1: Why a destructor cannot be marked as constexpr?
Because only trivial destructors are qualified for constexpr
Following is the relevant section of the draft
A destructor is trivial if it is not user-provided and if:
(5.4) — the destructor is not virtual,
(5.5) — all of the direct base classes of its class have trivial
destructors, and
(5.6) — 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.
Ques 2: If I do not provide a destructor, is the implicitly generated destructor constexpr?
Yes, because implicitly generated destructor is trivial type, so it is qualified for constexpr
Ques 3: If I declare a defaulted destructor (~X() = default;), is it automatically constexpr?
Indeed, this destructor is user-declared and implicitly-generated and thus it is qualified for constexpr.
I'm not able to find any direct reference that only trivial destructors are qualified for constexpr but if the destructor is not trivial then it is for sure that class type is not cv-qualified. So it kind of implicit as you can't define a destructor for cv-qualified class.
C++20 Update
Since C++20, user defined destructors can also be constexpr under certain conditions.
dcl.constexpr/3:
The definition of a constexpr function shall satisfy the following
requirements:
its return type (if any) shall be a literal type;
each of its parameter types shall be a literal type;
it shall not be a coroutine ([dcl.fct.def.coroutine]);
if the function is a constructor or destructor, its class shall not have any
virtual base classes;
its function-body shall not enclose ([stmt.pre])
a goto statement,
an identifier label ([stmt.label]),
a definition of a variable of non-literal type or of static or thread
storage duration.
If what you're looking for is reasoning behind the restriction, have a look at this paper which clearly states that the restriction is artificial - there is no intrinsic property of destructors that prevent them from working in constexpr contexts, and indeed compiler implementors agree that supporting them in constexpr contexts will be trivial to implement.
I guess the C++ standards committee originally placed the restriction in C++11 because they didn't want to deal with destructors at that time and it was easier to just rule them out entirely.
Since C++20, a constructor may be marked constexpr; I don’t know if it says anywhere specifically “a destructor may be constexpr”, but the draft standard includes the following text in section 9.2.5 paragraph 5:
The definition of a constexpr destructor whose function-body is not = delete shall additionally satisfy the
following requirement:
for every subobject of class type or (possibly multi-dimensional) array thereof, that class type shall
have a constexpr destructor.
This also now has a useful function because C++20 also allows new and delete in constexpr contexts, allowing things like vector and string to work at compile time without hacks (although I believe C++20 does not actually include changes to the standard library to allow for this, it is possible to implement something with the same API and behaviour as vector that works completely at compile time).
Why a destructor cannot be marked as constexpr?
The C++11 standard is specific about use of constexpr for consructors and non-static member function. It does not say anything specific about destructor. One may assume that destructors are to be treated as non-static member functions.
constexpr can be used only for const member functions. Since a destructor cannot be const member function, it cannot be qualified as a constexpr member function.
If I do not provide a destructor, is the implicitly generated destructor constexpr.
Since use of
constexpr ~X() = default;
is an error, it makes sense to me that the compiler generated destructor is not a constexpr function. I can't find anything in the standard to justify my statement. I am guessing.
If I declare a defaulted destructor (~X() = default;), is it automatically constexpr
I think not. Once again, I can't find anything in the standard to justify my statement. I am guessing.
FWIW, g++ compiles and builds the following program just fine.
struct X {
constexpr X(int i) : i_(i) {}
~X() = default;
int i_;
};
int main()
{
const X x(10);
}
Reference say's:
constexpr destructors
In most cases, in order to create an object of a type T in a constant
expression, the destruction of T must be trivial. However, non-trivial
destructors are an important component of modern C++, partly due to
widespread usage of the RAII idiom, which is also applicable in
constexpr evaluations. Non-trivial destructors could be supported in
constant expressions, as follows:
Allow destructors to be marked as constexpr
Make defaulted destructors constexpr if they only invoke constexpr destructors
For constexpr variables, require that evaluating the destructor is a constant expression (except that the object being destroyed may be
modified in its own destructor
However, no compelling use cases are known for such a feature, and
there would be a non-trivial implementation cost ensuring that
destructors are run at the right times.
A destructor can't be constexpr because constexpr functions can't have side effects and destructors by definition are only useful through side effects. In short, it would be useless to have a destructor that is constexpr.
A object cannot be constexpr if its destructor is non-trivial. A defaulted one, if trivial, will be considered constexpr
Live
From [class.dtor]
Each decl-specifier of the decl-specifier-seq of a destructor declaration (if any) shall be friend, inline, or virtual.
Missing from it, constexpr. So you could just take it as: because the standard says soTM