Friend function in template (why does this fail in Visual Studio but not GCC and Clang)

Viewed 394

Given the following (stripped down and contrived to highlight the problem):

#include <utility>

namespace Generic
{
    class Whatever
    {
    public:
        // ISSUE1 (member "swap()" has same name as non-member template)
        void swap()
        {
        }
    };

    // Forward declaration of class template "Test"
    template <class>
    class Test;

    // Prototype of function template "swap()" for class template "Test"
    template<class T>
    void swap(Test<T> &, Test<T> &);

    /////////////////////////////////////////////////////////////////
    // Class template "Test"
    /////////////////////////////////////////////////////////////////
    template <class T>
    class Test : public T
    {
    public:
        // Default constructor
        Test() = default;

        // Move constructor
        Test(Test &&test)
            : BaseClass(std::move(test))
        {
        }

        // Assignment operator
        Test& operator=(Test test)
        {
            // Note that the "Generic:" prefix here has no impact on the results
            Generic::swap(*this, test);

            return (*this);
        }

    private:
        using BaseClass = T;

        // ISSUE2 (qualified call - prefixing with "Generic:")
        friend void Generic::swap<T>(Test &, Test &);
    };

    /////////////////////////////////////////////////////////////////
    // Canonical swap function for class "Test" just above
    /////////////////////////////////////////////////////////////////
    template<class T>
    void swap(Test<T> &, Test<T> &)
    {
    }

} // namespace Generic

int main()
{
    using namespace Generic;
    Test<Whatever> test1;
    Test<Whatever> test2;
    test2 = std::move(test1);

    return 0;
}

Can anyone see any reason why it shouldn't compile. Note in particular ISSUE1 and ISSUE2 in the comments. These affect the results. There are 4 combinations of ISSUE1 and ISSUE2 to try and get things working (see table below), though I am aware of another way to correct things but these 4 combinations are what I want to focus on. Note that all 4 combinations compile cleanly in GCC and Clang, but only item 4 compiles cleanly in Visual Studio 2017 (latest version). The other combinations (1 to 3) fail with the errors seen in the RESULT column below (the compiler cites a problem with the ISSUE2 line in all 3 cases):

   ISSUE1                               ISSUE2                          RESULT
   ------                               ------                          ------
1) Leave unchanged                      Leave unchanged                 'Generic::Test': use of class template requires template argument list
                                                                        'Generic::swap': not a function

2) Leave unchanged                      Remove "Generic:" qualifier     syntax error: missing ';' before '<'
                                                                        'swap': illegal use of type 'void'
                                                                        'Generic::swap': 'friend' not permitted on data declarations
                                                                        'Generic::swap': redefinition; previous definition was 'function'
                                                                        unexpected token(s) preceding ';'

3) Comment out member "swap()"          Leave unchanged                 'Generic::Test': use of class template requires template argument list
                                                                        'Generic::swap': not a function

4) Comment out member "swap()"          Remove "Generic:" qualifier     Works!!!

Is Visual Studio just buggy or do GCC and Clang have it wrong (the former appears to be the case based on my understanding of the qualified/unqualified name lookup rules, ADL, etc.). Thanks.

3 Answers

Heh, found the blog: Two-phase name lookup support comes to MSVC.

Some relevant excerpts:

The original design of templates for C++ meant to do exactly what the term “template” implied: a template would stamp out families of classes and functions. It allowed and encouraged, but did not require, early checking of non-dependent names. Consequently, identifiers didn’t need to be looked up during parsing of the template definition. Instead, compilers were allowed to delay name lookup until the template was instantiated. Similarly, the syntax of a template didn’t need to be validated until instantiation. Essentially, the meaning of a name used in a template was not determined until the template was instantiated.

In accordance with these original rules, previous versions of MSVC did very limited template parsing. In particular, function template bodies were not parsed at all until instantiation. The compiler recorded the body of a template as a stream of tokens that was replayed when it was needed during instantiation of a template where it might be a candidate.

The article then goes on to post some code very similar to n.m.’s minimal example.

It’s important to note that overloads declared after the point of the template’s definition but before the point of the template’s instantiation are only considered if they are found through argument-dependent lookup. MSVC previously didn’t do argument-dependent lookup separately from ordinary, unqualified lookup so this change in behavior may be surprising.

Finally, a note about making MSVC use the updated two-phase template parsing:

You’ll need to use the /permissive- conformance switch to enable two-phase lookup in the MSVC compiler included with Visual Studio 2017 “15.3”. Two-phase name lookup drastically changes the meaning of some code so the feature is not enabled by default in the current version of MSVC.

Changing...

friend void Generic::swap<T>(Test &, Test &);

to

friend void Generic::swap<T>(Test<T> &, Test<T> &);

... allows this to compile. However as mentioned by n.m, Test is an injected class name, and one should be able to omit T inside it's class definition

Here's the real minimal example.

   template <class> struct Test;
   template <class T> void moo(Test<T>&){};

   template <class T>
   struct Test
   {
       friend void ::moo<T>(Test &);
   };

   Test<int> x;

Lookup rules have nothing to do to the problem, which is clearly a compiler bug. It can be inhibited by seemingly unrelated changes, like removing :: from the friend declaration.

The reason of the failure is revealed by the error message:

'Test': use of class template requires template argument list

Since Test is an injected-class-name, it can be used without a template argument list, but MSVC apparently forgets about this rule under some unknown set of circumstances. This seems to only happen within a friend declaration (not any other declaration or definition) when the declared name is qualified with the namespace name. Determining the exact conditions that trigger the bug is not very interesting or productive though.

The second error message

'moo': not a function

is an induced one: since the compiler finds an "error" in the friend declaration of moo, the declaration is labelled as erroneous. The same "not a function" error is emitted if you replace Test in the parameter list with an undeclared identifier.

Using Test<T> is a workaround.

Presence of a base class member with the same name triggers what seems to be a different issue in MSVC which is related to the (lack of) two-phase lookup. Here's the minimal example:

template <class> void moo() {}

struct Base
{
    void moo() {}
};

template <class T>
struct Test : T
{
    friend void moo<T>();
};

Test<Base> tb;

In this example, the compiler fails to bind moo to the global template at the first phase of name resolution, and gets confused when the template is instantiated and moo gets incorrectly resolved to the member of the base class. This works differently from the previous issue, where moo is bound to the global template whether or not two phase lookup is implemented. Unlike the first example, here the bug is only triggered if moo in the friend declaration is unqualified.

Related