Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Is a class template's name in scope for a qualified out-of-line destructor's definition?

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.

like image 733
Human-Compiler Avatar asked Aug 12 '21 04:08

Human-Compiler


People also ask

What is class template in C++ with example?

A class template can be declared without being defined by using an elaborated type specifier. For example: template<class L, class T> class Key; This reserves the name as a class template name.

What are template parameters?

In UML models, template parameters are formal parameters that once bound to actual values, called template arguments, make templates usable model elements. You can use template parameters to create general definitions of particular types of template.

How do you declare a template?

To instantiate a template class explicitly, follow the template keyword by a declaration (not definition) for the class, with the class identifier followed by the template arguments. template class Array<char>; template class String<19>; When you explicitly instantiate a class, all of its members are also instantiated.

How many types of templates are there in C++?

There are three kinds of templates: function templates, class templates and, since C++14, variable templates. Since C++11, templates may be either variadic or non-variadic; in earlier versions of C++ they are always non-variadic.


1 Answers

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.

like image 75
Human-Compiler Avatar answered Oct 17 '22 02:10

Human-Compiler