I was writing an out-of-class destructor definition for a class template when I noticed that the program compiles with clang with c++17 and c++20 and also with gcc with c++17 but rejected with gcc c++20. Demo.
template<typename T>
struct C
{
~C();
};
template<typename T>
C<T>::~C<T>() //accepted by compilers
{
}
int main()
{
C<int> c;;
}
The result of the above program is summarized in the below table:
Compiler
C++ Version
Accepts-Code
GCC
C++17
Yes
GCC
C++20
No
GCC
C++2b
No
Clang
C++17
Yes
Clang
C++20
Yes
Clang
C++2b
Yes
MSVC
C++17
Yes
MSVC
C++20
Yes
As we can see in the above both of the compilers accept the code except that gcc with c++20 and onwards reject it with the error error: template-id not allowed for destructor.
So, my question is which compiler is right here(if any).
The program is ill-formed atleast starting from c++20 and clang and msvc are wrong in accepting the code with c++20 and onwards.
Note that the change in wording for class.dtor was introduced in C++23 via p1787r6-class.dtor and seems to be a DR for C++20.
So, the code is ill-formed from C++20 and onwards which can be seen from: class.dtor#1.2 which states that:
1 A declaration whose declarator-id has an unqualified-id that begins with a ~ declares a prospective destructor; its declarator shall be a function declarator ([dcl.fct]) of the form
ptr-declarator ( parameter-declaration-clause ) noexcept-specifieropt attribute-specifier-seqopt
where the ptr-declarator consists solely of an id-expression, an optional attribute-specifier-seq, and optional surrounding parentheses, and the id-expression has one of the following forms:
1.2 otherwise, the id-expression is nested-name-specifier ~class-name and the class-name is the injected-class-name of the class nominated by the nested-name-specifier.
(emphasis mine)
And since the class-name is the injected-class-name C and not C<T> in our example, the correct way to write an out of class implementation for the destructor would be as shown below:
template<typename T>
struct C
{
~C(); //this is an ordinary destructor(meaning it is not templated)
};
template<typename T>
//-----v----------------> C is the injected-class-name and not C<T>
C<T>::~C()
{
}
int main()
{
C<int> c;;
}
Demo
Here is the clang bug report:
Clang accepts invalid out of class definition for destructor
Here is msvc bug report:
MSVC accepts invalid out of class definition for a destructor of a class template with c++20
For further reading
One can also refer to:
Why destructor cannot be template?
Error: Out-of-line constructor cannot have template arguments C++.
template<typename T> someclass<T>::someclass<T>() is not allowed when providing an out of class definition for a constructor and a destructor with any c++ version.
I don't think it's true that this was invalid in C++17.
CWG 1435 introduced the wording that allowed the code (that wording can still be seen in the context for the [class.dtor] changes in CWG 2337):
in a declaration at namespace scope or in a friend declaration, the id-expression is nested-name-specifier ~class-name and the class-name names the same class as the nested-name-specifier."
This allows the code, because C<T>::~C<T> is an id-expression of that form. The class-name in the destructor ~C<T> names the same class as in C<T>::. That wording was present in C++17 and C++20.
P1787R6 Declarations and where to find them changed that wording to:
otherwise, the id-expression is nested-name-specifier ~class-name and the class-name is the injected-class-name of the class nominated by the nested-name-specifier."
This no longer allows ~C<T> because the injected-class-name is just C.
This seems like a breaking change that was not obvious from the revision history of the r5 paper, which includes:
Required destructor declarations to use the injected-class-name, avoiding name lookup
Simplified lookup for destructors
Specified that < begins a template argument list in a destructor name
All compilers (GCC, Clang, EDG and MSVC) accept the program in C++17 mode, and I don't think it's at all clear that the code was always invalid in older standards, as OP claims. It seems to be a possibly-unintentional change in C++23. Edit: I'm reliably informed by Jason Merrill that it was an intentional change for C++23, but not a DR for C++17. See his comment on the other answer.
Related
Recent versions of clang (since clang-11) issue a warning when compiled with -pedantic for the following segment of code:
namespace foo {
template <int A>
struct Bar {
~Bar();
};
} // namespace foo
template <int A>
foo::Bar<A>::~Bar(){}
With the generated warning (and error with -Werror) being:
<source>:10:12: error: ISO C++ requires the name after '::~' to be found in the same scope as the name before '::~' [-Werror,-Wdtor-name]
foo::Bar<A>::~Bar(){}
~~~~~~~~~~~^~
::Bar
Live Example
clang is the first compiler I've seen to issue this diagnostic, and as far as I'm aware, what was written above is completely valid C++. Two different ways that seem to suppress this is to either define within the same namespace or explicitly qualify the destructor name -- such as:
...
template <int A>
foo::Bar<A>::~Bar<A>(){}
Is Clang's diagnostic here correct? It's my understanding that the type's name would absolutely be in the correct name-scope as foo::Bar<A>::~ -- and that the qualification of ~Bar<A>() should be unnecessary.
From what I have learned since posting the question, this warning is, strictly-speaking, correct -- though it most likely a defect in the wording of the standard.
According to Richard Smith in LLVM Bug 46979:
The diagnostic is correct, per the standard rules as written; strictly-speaking, the C++ rules require this destructor to be written as
template<typename T>
A::B<T>::~B<T>()
Per C++ [basic.lookup.qual]p6:
In a qualified-id of the form:
nested-name-specifier[opt] type-name :: ~ type-name
the second type-name is looked up in the same scope as the first.
This means that the second B is looked up in class A, which finds only the class template B and not the injected-class-name.
This is not an especially useful rule, and likely isn't the intended rule, which is why this diagnostic (along with a bunch of similar diagnostics for plausible but formally incorrect destructor names) is disabled by default but included in -pedantic.
Looking further into this, I can find the mentioned passage for [basic.lookup.qual]/6 in the C++20 standard, but it appears drafts for C++23 have changed this -- which points towards this most likely being a defect.
In drafts for [basic.lookup.qual] for C++23, this whole section has been overhauled and is currently, at the time of writing, replaced with [basic.lookup.qual]/4 which states:
If a qualified name Q follows a ~:
(4.1) If Q is a member-qualified name, it undergoes unqualified lookup as well as qualified lookup.
(4.2) Otherwise, its nested-name-specifier N shall nominate a type.
If N has another nested-name-specifier S, Q is looked up as if its lookup context were that nominated by S.
(4.3) Otherwise, if the terminal name of N is a member-qualified name M, Q is looked up as if ~Q appeared in place of M (as above).
(4.4) Otherwise, Q undergoes unqualified lookup.
(4.5) Each lookup for Q considers only types (if Q is not followed by a <) and templates whose specializations are types.
If it finds nothing or is ambiguous, it is discarded.
(4.6) The type-name that is or contains Q shall refer to its (original) lookup context (ignoring cv-qualification) under the interpretation established by at least one (successful) lookup performed.
[Example 4:
struct C {
typedef int I;
};
typedef int I1, I2;
extern int* p;
extern int* q;
void f() {
p->C::I::~I(); // I is looked up in the scope of C
q->I1::~I2(); // I2 is found by unqualified lookup
}
struct A {
~A();
};
typedef A AB;
int main() {
AB* p;
p->AB::~AB(); // explicitly calls the destructor for A
}
— end example]
(The full quote has been posted here since this is from a draft, and the wording may be subject to change in the future)
This appears to explicitly correct this issue by ensuring that lookup is performed as one would expect.
So basically the diagnostic is correct under older versions of C++ due to a defect in the wording -- but C++23 appears to change that. I am not sure whether this will be retroactively fixed in older versions as a defect given that it appears no compiler actually follows this pedantic requirement.
I have the following vector class
#include <cmath>
#include <cstdlib>
#include <string>
#include <array>
template<typename T>
class Vec2
{
public:
Vec2();
Vec2(T x1, T x2);
Vec2(const T * data);
using arr_t = std::array<T,2>;
Vec2<T>(const arr_t &o) : _x(o) {}
private:
arr_t _x;
};
It compiles fine in c++20 standard with clang10/linux, but then I have the following error on a Windows MinGW (gcc 11.2) port:
vec2.h:55:17: error: expected unqualified-id before 'const'
55 | Vec2<T>(const arr_t &o) : _x(o) {}
| ^~~~~
Someone in the comments noticed the extra in the last constructor declaration, which shouldn't be there
Is this a clang bug to accept this? (it compiles and run fine)
The shown snippet is valid for Pre-C++20 but not valid from C++20 & onwards as explained below.
Pre-C++20
From class.ctor#1.2:
1 -- Constructors do not have names. In a declaration of a constructor, the declarator is a function declarator of the form:
ptr-declarator ( parameter-declaration-clause ) noexcept-specifieropt attribute-specifier-seqopt
where the ptr-declarator consists solely of an id-expression, an optional attribute-specifier-seq, and optional surrounding parentheses, and the id-expression has one of the following forms:
1.2 -- in a member-declaration that belongs to the member-specification of a class template but is not a friend declaration, the id-expression is a class-name that names the current instantiation of the immediately-enclosing class template; or
(end quote)
This means that in C++17, we are allowed to use Vec2<T>(const arr_t &o); as the constructor declaration.
C++20
From class.ctor#1.1:
1 -- A constructor is introduced by a declaration whose declarator is a function declarator ([dcl.fct]) of the form:
ptr-declarator ( parameter-declaration-clause ) noexcept-specifieropt attribute-specifier-seqopt
where the ptr-declarator consists solely of an id-expression, an optional attribute-specifier-seq, and optional surrounding parentheses, and the id-expression has one of the following forms:
1.1 -- in a member-declaration that belongs to the member-specification of a class or class template but is not a friend declaration ([class.friend]), the id-expression is the injected-class-name ([class.pre]) of the immediately-enclosing entity or
So as we can see, the injected class name(which is Vec2 and not Vec2<T> in your example) is needed for declaring the ctor of a class template.
This means that your given code is not valid for C++20.
The same is also mentioned at diff.cpp17.class#2:
Affected subclauses: [class.ctor] and [class.dtor]
Change: A simple-template-id is no longer valid as the declarator-id of a constructor or destructor.
Rationale: Remove potentially error-prone option for redundancy.
Effect on original feature: Valid C++ 2017 code may fail to compile in this revision of C++.
For example:
template<class T>
struct A {
A<T>(); // error: simple-template-id not allowed for constructor
A(int); // OK, injected-class-name used
~A<T>(); // error: simple-template-id not allowed for destructor
};
Edit
Your code was well-formed per the original C++20 grammar, however there is an accepted defect report in C++20 that disallows a simple-template-id in a constructor declaration.
GCC is ahead of the game here in implementing the DR before the other compilers, it seems. (gcc ref).
The resolution is to change
Vec2<T>(const arr_t &o) : _x(o) {}
into
Vec2(const arr_t &o) : _x(o) {}
Original answer
Your code is well-formed per the C++ grammar.
Per [class.ctor.general], a class constructor that is defined within the class declaration should be an "id-expression" that consists of the "injected class name", which in most cases is meant to simply refer to the name of the class sans any template arguments (formally, an identifier instead of a simple-template-id).
However, the C++ grammar defines class name as being either a plain identifier (Vec2) or a simple-template-id (Vec2<T>) ([class.pre]).
I will note that clang (trunk) and MSVC (latest) both accept your code, while gcc (trunk) fails. I believe this is a gcc bug.
Recent versions of clang (since clang-11) issue a warning when compiled with -pedantic for the following segment of code:
namespace foo {
template <int A>
struct Bar {
~Bar();
};
} // namespace foo
template <int A>
foo::Bar<A>::~Bar(){}
With the generated warning (and error with -Werror) being:
<source>:10:12: error: ISO C++ requires the name after '::~' to be found in the same scope as the name before '::~' [-Werror,-Wdtor-name]
foo::Bar<A>::~Bar(){}
~~~~~~~~~~~^~
::Bar
Live Example
clang is the first compiler I've seen to issue this diagnostic, and as far as I'm aware, what was written above is completely valid C++. Two different ways that seem to suppress this is to either define within the same namespace or explicitly qualify the destructor name -- such as:
...
template <int A>
foo::Bar<A>::~Bar<A>(){}
Is Clang's diagnostic here correct? It's my understanding that the type's name would absolutely be in the correct name-scope as foo::Bar<A>::~ -- and that the qualification of ~Bar<A>() should be unnecessary.
From what I have learned since posting the question, this warning is, strictly-speaking, correct -- though it most likely a defect in the wording of the standard.
According to Richard Smith in LLVM Bug 46979:
The diagnostic is correct, per the standard rules as written; strictly-speaking, the C++ rules require this destructor to be written as
template<typename T>
A::B<T>::~B<T>()
Per C++ [basic.lookup.qual]p6:
In a qualified-id of the form:
nested-name-specifier[opt] type-name :: ~ type-name
the second type-name is looked up in the same scope as the first.
This means that the second B is looked up in class A, which finds only the class template B and not the injected-class-name.
This is not an especially useful rule, and likely isn't the intended rule, which is why this diagnostic (along with a bunch of similar diagnostics for plausible but formally incorrect destructor names) is disabled by default but included in -pedantic.
Looking further into this, I can find the mentioned passage for [basic.lookup.qual]/6 in the C++20 standard, but it appears drafts for C++23 have changed this -- which points towards this most likely being a defect.
In drafts for [basic.lookup.qual] for C++23, this whole section has been overhauled and is currently, at the time of writing, replaced with [basic.lookup.qual]/4 which states:
If a qualified name Q follows a ~:
(4.1) If Q is a member-qualified name, it undergoes unqualified lookup as well as qualified lookup.
(4.2) Otherwise, its nested-name-specifier N shall nominate a type.
If N has another nested-name-specifier S, Q is looked up as if its lookup context were that nominated by S.
(4.3) Otherwise, if the terminal name of N is a member-qualified name M, Q is looked up as if ~Q appeared in place of M (as above).
(4.4) Otherwise, Q undergoes unqualified lookup.
(4.5) Each lookup for Q considers only types (if Q is not followed by a <) and templates whose specializations are types.
If it finds nothing or is ambiguous, it is discarded.
(4.6) The type-name that is or contains Q shall refer to its (original) lookup context (ignoring cv-qualification) under the interpretation established by at least one (successful) lookup performed.
[Example 4:
struct C {
typedef int I;
};
typedef int I1, I2;
extern int* p;
extern int* q;
void f() {
p->C::I::~I(); // I is looked up in the scope of C
q->I1::~I2(); // I2 is found by unqualified lookup
}
struct A {
~A();
};
typedef A AB;
int main() {
AB* p;
p->AB::~AB(); // explicitly calls the destructor for A
}
— end example]
(The full quote has been posted here since this is from a draft, and the wording may be subject to change in the future)
This appears to explicitly correct this issue by ensuring that lookup is performed as one would expect.
So basically the diagnostic is correct under older versions of C++ due to a defect in the wording -- but C++23 appears to change that. I am not sure whether this will be retroactively fixed in older versions as a defect given that it appears no compiler actually follows this pedantic requirement.
Consider the following example:
namespace N {
template<class>
struct C { };
template<int, class T>
void foo(C<T>);
}
template<class T>
void bar(N::C<T> c) {
foo<0>(c);
}
int main() {
N::C<void> c;
bar(c);
}
Both GCC and Clang fail to compile this code under C++17 standard (with -Werror), because (according to my understanding) in C++17 ADL doesn't work when explicit template arguments <...> are present (unless a name is already established as a template name), so foo is a non-dependent name that is not found.
In C++20, the ADL rules have changes, and explicit template arguments don't prevent ADL. Now it seems that foo becomes a dependent name that should be resolvable via ADL. However, GCC and Clang have different opinions about the validity of this code. CLang compiles it without errors, but GCC (10.2, -std=c++2a) complains:
error: 'foo' was not declared in this scope; did you mean 'N::foo'?
In C++17 mode, Clang produces the following warning:
warning: use of function template name with no prior declaration in function call with explicit template arguments is a C++20 extension
Demo.
I have three related questions:
Which compiler is right and why?
In C++17, is foo in foo<0>(c) considered as a dependent name?
In C++20, is foo in foo<0>(c) considered as a dependent name?
This is P0846, which is a C++20 feature. It appears that gcc does not implement this yet.
It's not a question of dependent name or not, it's a question of does the compiler know that foo refers to a template or not, and so is foo< doing a comparison or is it starting to do template parameters.
In C++17, the compiler had to already know that foo was a template-name (which you could accomplish by adding using N::foo;) in order to perform ADL, in C++20 that's no longer true - now the rule is that if unqualified lookup finds a template or nothing, we also consider it to be a template.
The dependence of foo didn't change as a result of this paper. In foo<0>(c);, foo is still a dependent name. The rule in [temp.dep] from C++17 was:
In an expression of the form:
postfix-expression ( expression-listopt )
where the postfix-expression is an unqualified-id, the unqualified-id denotes a dependent name if
any of the expressions in the expression-list is a pack expansion,
any of the expressions or braced-init-lists in the expression-list is type-dependent, or
the unqualified-id is a template-id in which any of the template arguments depends on a template parameter.
The second bullet applies here - c is type-dependent. The C++20 wording is the same. The issue here wasn't that foo wasn't dependent in C++17. It's just that the rule was when we foo<, we don't know that foo is a template, so it's considered the less-than-operator, and that then fails.
This question already has an answer here:
Is it possible to call templated user-defined conversion operator with explicit template arguments?
(1 answer)
Closed 2 years ago.
Consider the following program:
struct A {
template <typename T>
operator T() { return T{}; }
};
int main() {
(void) A{}.operator int(); // (A)
(void) A{}.template operator int(); // (B)
}
(A) is accepted by both GCC and Clang, whereas (B) is accepted only by GCC but rejected by Clang with the following error message:
error: expected template name after 'template' keyword in nested name specifier
(void) A{}.template operator int(); // (B)
^~~~~~~~~~~~
Afaict, (B) should be legal, as per [temp.names]/5:
A name prefixed by the keyword template shall be a template-id or the name shall refer to a class template or an alias template. [ Note: The keyword template may not be applied to non-template members of class templates. — end note ] [ Note: As is the case with the typename prefix, the template prefix is allowed in cases where it is not strictly necessary; i.e., when the nested-name-specifier or the expression on the left of the -> or . is not dependent on a template-parameter, or the use does not appear in the scope of a template. — end note ]
and as the prohibitions governed by [temp.names]/4 does not apply:
The keyword template is said to appear at the top level in a qualified-id if it appears outside of a template-argument-list or decltype-specifier. [...] an optional keyword template appearing at the top level is ignored. [...]
and, at most, only state that the keyword should be ignored (but not that the program is ill-formed).
I have not found any clause in [class.conv.fct] or [temp.deduct.conv] that conflicts with this argument.
Question
Is it legal to prefix explicit access to a conversion function template with the template keyword?
I have tested and repeated the compilers' behaviour above with various GCC and Clang versions for various language standard versions, but for the scope of this question, we may focus on GCC 10.1.0 and Clang 10.0.0 for -std=c++17.
GCC is wrong to accept the program: a conversion function template name (conversion-function-id) is not a template-id
This is CWG Defect Report 96, which has, as of GCC 10, has not yet been addressed. The related bug GCC ticket 55588 mentions that it is being implemented for GCC 11.
As is covered in the answer to Is it possible to call templated user-defined conversion operator with explicit template arguments?, a conversion function template name does not name a template-id; quoting the grammar from template-id from [temp.names]/1:
simple-template-id:
template-name < template-argument-list_opt>
template-id:
simple-template-id
operator-function-id < template-argument-list_opt>
literal-operator-id < template-argument-list_opt>
template-name:
identifier
Thus, as is quoted in the OP, as per [temp.names]/5, the template keyword may not be used to prefix the name of a conversion function template, as the latter is not a template-id.
It may be interesting to point out that Clang does not reject using the keyword when referring to an operator-function-id (which is a template-id):
struct A {
template <typename T>
T operator+() { return T{}; }
};
int main() {
(void) A{}.operator+<int>(); // (A)
(void) A{}.template operator+<int>(); // (B)
}
That GCC accepts the OP's program is, as covered above, a bug, as it violates [temp.names]/5.