模板对象的模板友元函数和命名空间

template object's template friend functions and namespaces

在以下 C++ 示例代码中,GCC 6 和 Clang 3.8 对正确行为的看法存在分歧:

这个人为的例子 "works" —— 就像 GCC 中的 test() 函数 returns o.p 一样。在 clang 中,它调用(未定义的)函数 get<int, int, float, double>:

template<typename ...Args>
class obj {
 bool p = false;

 template<typename T, typename... Args2>
 friend T get(const obj<Args2...> &o) { return o.p; }
};

template<typename T, typename... Args>
T get(const obj<Args...> &o);


bool test(const obj<int, float, double> &a) {
 return get<int>(a);
}

将相同的代码放入命名空间会导致 GCC 执行与 clang 相同的操作。

namespace ns {

template<typename ...Args>
class obj {
 bool p = false;

 template<typename T, typename... Args2>
 friend T get(const obj<Args2...> &o) { return o.p; }
};

template<typename T, typename... Args>
T get(const obj<Args...> &o);

}

bool test(const ns::obj<int, float, double> &a) {
 return ns::get<int>(a);
}

https://godbolt.org/g/sWrXQO and https://godbolt.org/g/9tIXwe

哪个编译器是 "correct" 并且通常有一种方法可以 内联定义 友元成员模板函数而无需声明它然后单独定义它。也就是说,像这样的东西:

struct Foo {
 friend bool bar() { return true; } // declares *and* defines a free function bar
 template<typename T> T bar2() { return true; }  // doesn't work!
};

class 模板中定义的 friend 函数模板有两个未解决的问题:1545 and 2174。前者质疑它的有效程度,后者质疑基于这些函数模板的实际实例化可能出现的 ODR 违规。我不确定哪个编译器是正确的(以前认为两者都是错误的),但在这种情况下,标准中可能只是低估或低估了正确的行为是什么。

代码应该 理想地编译(待解决的问题):

template<typename ...Args>
class obj {
 bool p = false;

 template<typename T, typename... Args2>
 friend T get(const obj<Args2...> &o) { return o.p; }
};

template<typename T, typename... Args>
T get(const obj<Args...> &o);

friend 声明首先声明 get,因此这将创建最内层封闭命名空间的新成员:::get。外部声明只是重新声明同一个函数,所以真的只有一个::get。来自 [temp.over.link]:

Two expressions involving template parameters are considered equivalent if two function definitions containing the expressions would satisfy the one-definition rule (3.2), except that the tokens used to name the template parameters may differ as long as a token used to name a template parameter in one expression is replaced by another token that names the same template parameter in the other expression. For determining whether two dependent names (14.6.2) are equivalent, only the name itself is considered, not the result of name lookup in the context of the template.

使用不同的模板参数名称(Args... vs Args2...)没问题 - 函数模板的第二个声明 ::get 是有效的并且允许通过查找找到它。

这将我们带到:

bool test(const obj<int, float, double> &a) {
#ifdef UNQUAL
    return get<int>(a);     // unqualified
#else
    return ::get<int>(a);   // qualified
#endif
}

这两个 应该 工作 - 因为我们重新声明函数在命名空间范围内,我们甚至不必担心 ADL。两个调用都应该找到 ::get<int>(obj<int, float, double> ),这是一个 friend,因此代码应该编译并且 link。 gcc 允许不合格的调用但不允许合格的调用,clang 两者都不允许。

我们可以通过简单地 而不是 在 class:

中定义函数模板来完全回避这两个 CWG 问题
template<typename ...Args>
class obj {
 bool p = false;

 template<typename T, typename... Args2>
 friend T get(const obj<Args2...> &o);
};

template<typename T, typename... Args>
T get(const obj<Args...> &o) { return o.p; }

使用此公式,两个编译都允许 合格和不合格的 get 调用。


如果我们重写代码使得 obj 是 class 而不是 class 模板,但其他条件相同:

class obj {
    bool p = false;

    template <class T>
    friend T get(const obj& o) { return o.p; }
};

template <class T> T get(const obj& );

bool test(const obj& a) {
#ifdef UNQUAL
    return get<int>(a);
#else
    return ::get<int>(a);
#endif
}

两种编译器都允许两种调用。但据我所知,obj 是普通 class 和 class 模板之间的规则没有区别。