5

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).

Jason
  • 36,170
  • 5
  • 26
  • 60
  • 5
    isn't it `C::~C`? – apple apple Oct 02 '22 at 19:13
  • @appleapple yep https://godbolt.org/z/cGP9ezs51 – Alan Birtles Oct 02 '22 at 19:14
  • @appleapple Yes, as explained in the answer below. – Jason Oct 02 '22 at 19:15
  • msvc complains it's obsolete syntax https://godbolt.org/z/Yv64rxKaW – Alan Birtles Oct 02 '22 at 19:15
  • Dup of [Can class template constructors have a redundant template parameter list in c++20](https://stackoverflow.com/questions/63513984/can-class-template-constructors-have-a-redundant-template-parameter-list-in-c2) – Language Lawyer Oct 02 '22 at 20:45
  • @LanguageLawyer No it is not a dupe of that. Note carefully what you suggested is for declarations in member specification but not for declarations at namespace scope. The program is ill-formed whether it be c++17 or c++20. So, these two are different questions. In your suggested question the program is ill-formed only with c++20 onwards but with my question the program is ill-formed in c++17 also. – Jason Oct 03 '22 at 05:58

2 Answers2

4

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.

Jason
  • 36,170
  • 5
  • 26
  • 60
  • **Second** and most importantly, this means that the program in my question is ill-formed in all c++ versions including c++17 while the program in the suggested dupe is ill-formed only from c++20 onwards. – Jason Oct 03 '22 at 06:03
  • 2
    No, [class.dtor] (1.3) in C++17 (and C++20, and C++14) says the code is valid. You can't quote wording from the C++23 draft and claim it applies to C++17, which has different wording. – Jonathan Wakely Oct 03 '22 at 12:47
  • @JonathanWakely As i said under comment section of your answer, it is a defect in the old standards for which the new wording https://eel.is/c++draft/class.dtor#1.2 was introduced. Also, note that the intention in the old standard was already there to make `C::~C()` invalid. This is because a destructor is a non template member function that is just like other nontemplate member function of a class template. And when we define any other nontemplate member function of class template outside the class then we're not allowed to write: `C::membername`. – Jason Oct 03 '22 at 12:49
  • 2
    Saying it's a defect in the old standard doesn't change the content of the old standard. Even changes that the committee specifically adopts as defect reports only apply to the most recent published standard (i.e. C++20); extending those changes to earlier standard modes is at the discretion of implementers. In general, we try to avoid breaking existing code that was well-formed under the published standard. – Jason Merrill Oct 03 '22 at 14:10
1

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.

Jonathan Wakely
  • 166,810
  • 27
  • 341
  • 521
  • *"I don't think it's true that this was valid in C++17...."* Yes i agree as also said in my answer. – Jason Oct 03 '22 at 11:45
  • Oops, typo, I meant **invalid**. It **was** valid in C++17. – Jonathan Wakely Oct 03 '22 at 12:44
  • Oh, i see...... – Jason Oct 03 '22 at 12:45
  • Your answer is wrong, just look at [class.dtor] (1.3) in C++17, which clearly allows it. – Jonathan Wakely Oct 03 '22 at 12:45
  • Yes, i already did many times. I think it is a defect in the old standards due to which https://wg21.link/p1787r6 changed the wording. – Jason Oct 03 '22 at 12:46
  • I see no evidence to justify that claim. The changes to [class.dtor] resolved [CWG 399](https://cplusplus.github.io/CWG/issues/399.html) but none of the problems shown in that issue related to `~C()`. I'm not convinced changing it was intentional, and certainly not convinced it is a DR for older standards (CWG 2337 wasn't, why should this be?) – Jonathan Wakely Oct 03 '22 at 12:57
  • The downvote is because there is enough evidence that this was not allowed in older standards. For example, one of the answer clearly provides [this source link](https://youtu.be/LMP_sxOaz6g?t=1631) that says that outside the class we cannot write `C::~C()` but inside the class we can write `~C()`. Second, the destructor is an ordinary member function just like any other nontemplate member function of a class template. And when defining a member function out of class of a class template we're not allowed to write `C::func()`. The new C++23 wording was added to prevent this. – Alex Oct 03 '22 at 13:09
  • 1
    A youtube video is not the standard. Unlike `func` the destructor uses the name of the class, and `C` is ... guess what ... the name of the class. The [GCC bug](https://gcc.gnu.org/bugzilla/show_bug.cgi?id=107126#c8) has been rejected, although a `-Wc++20-compat` warning will be added for this case. – Jonathan Wakely Oct 03 '22 at 14:36