继承 C++14 中的模板化运算符 =:g++ 和 clang++ 的不同行为

Inheriting templated operator= in C++14: different behaviour with g++ and clang++

本文关键字:clang++ g++ C++14 运算符 继承      更新时间:2023-10-16

我有这段代码,它可以在GCC 9.1中按预期工作:

#include <type_traits>
template< typename T >
class A
{
protected:
T value;
public:
template< typename U,
typename...,
typename = std::enable_if_t< std::is_fundamental< U >::value > >
A& operator=(U v)
{
value = v;
return *this;
}
};
template< typename T >
class B : public A<T>
{
public:
using A<T>::operator=;
template< typename U,
typename...,
typename = std::enable_if_t< ! std::is_fundamental< U >::value > >
B& operator=(U v)
{
this->value = v;
return *this;
}
};
int main()
{
B<int> obj;
obj = 2;
}

(在实践中,我们会在B::operator=做一些花哨的事情,甚至使用不同类型的特征来enable_if,但这是最简单的可重现示例。

问题是 thtat Clang 8.0.1 给出了一个错误,不知何故不考虑父类的operator=,尽管孩子有using A<T>::operator=;

test.cpp:39:9: error: no viable overloaded '='
obj = 2;
~~~ ^ ~
test.cpp:4:7: note: candidate function (the implicit copy assignment operator) not viable:
no known conversion from 'int' to 'const A<int>' for 1st argument
class A
^
test.cpp:4:7: note: candidate function (the implicit move assignment operator) not viable:
no known conversion from 'int' to 'A<int>' for 1st argument
class A
^
test.cpp:20:7: note: candidate function (the implicit copy assignment operator) not
viable: no known conversion from 'int' to 'const B<int>' for 1st argument
class B : public A<T>
^
test.cpp:20:7: note: candidate function (the implicit move assignment operator) not
viable: no known conversion from 'int' to 'B<int>' for 1st argument
class B : public A<T>
^
test.cpp:28:8: note: candidate template ignored: requirement
'!std::is_fundamental<int>::value' was not satisfied [with U = int, $1 = <>]
B& operator=(U v)
^
1 error generated.

根据标准,哪个编译器是正确的?(我正在与-std=c++14一起编译。我应该如何更改代码以使其正确?

考虑以下简化代码:

#include <iostream>
struct A
{
template <int n = 1> void foo() { std::cout << n; }
};
struct B : public A
{
using A::foo;
template <int n = 2> void foo() { std::cout << n; }
};
int main()
{
B obj;
obj.foo();
}

这将打印 2,就像两个编译器一样。

如果派生类已经具有具有相同签名的类,则它将隐藏或重写using声明引入的派生类。分配运算符的签名表面上是相同的。考虑以下片段:

template <typename U, 
typename = std::enable_if_t<std::is_fundamental<U>::value>>
void bar(U) {}
template <typename U, 
typename = std::enable_if_t<!std::is_fundamental<U>::value>>
void bar(U) {}

这会导致两个编译器bar的重定义错误。

但是,如果更改其中一个模板中的返回类型,错误就会消失!

是时候仔细研究标准了。

当 using-declarator 将基类中的声明引入派生类时,成员函数和 派生类中的成员函数模板重写和/或隐藏成员函数和成员函数 具有相同名称、参数类型列表 (11.3.5)、简历限定符和引用限定符(如果有)的模板 基类(而不是冲突)。此类隐藏或重写的声明被排除在 使用声明符引入的声明

现在,就模板而言,这听起来很可疑。如果不比较模板参数列表,怎么能比较两个参数类型列表?前者取决于后者。事实上,上面一段说:

如果命名空间作用域或块作用域中的函数声明与 using 声明引入的函数具有相同的名称和相同的参数类型列表 (11.3.5),并且声明未声明 相同的功能,程序格式不正确。如果命名空间范围内的函数模板声明具有相同的 名称、参数类型列表、返回类型和模板参数列表作为函数模板引入 使用声明,则程序格式不正确。

这更有意义。如果两个模板的模板参数列表相同,则两个模板是相同的,其他所有内容都相同......但是等等,这包括返回类型!如果两个模板的名称和签名中的所有内容(包括返回类型(但不包括默认参数值))相同,则两个模板相同。然后一个可以与另一个冲突或隐藏另一个。

那么,如果我们更改 B 中赋值运算符的返回类型并使其与 A 中的返回类型相同,会发生什么?GCC 停止接受代码

所以我的结论是这样的:

  1. 当涉及到使用声明隐藏其他模板的模板时,该标准尚不清楚。如果它的意思是从比较中排除模板参数,它应该这样说,并澄清可能的影响。例如,函数是否可以隐藏函数模板,反之亦然?在任何情况下,命名空间范围内的using与将基类名称引入派生类的using之间的标准语言之间存在无法解释的不一致。
  2. GCC 似乎采用命名空间范围内的using规则,并将其应用于基/派生类的上下文中。
  3. 其他
  4. 编译器执行其他操作。目前还不太清楚到底是什么;正如标准的字母所说,可能会在不考虑模板参数(或返回类型)的情况下比较参数类型列表,但我不确定这是否有意义。

注意: 我觉得这个答案是错误的,n.m.的答案是正确的。 我 会保留这个答案,因为我不确定,但请去检查一下 这个答案。


Per [namespace.udecl]/15:

using 声明将基类中的名称引入 派生类范围、成员函数和成员函数模板 派生类重写和/或隐藏成员函数和成员 具有相同名称、参数类型列表的函数模板 ([dcl.fct])、简历资格和参考限定符(如果有的话) 类(而不是冲突)。

派生类B中声明的operator=A中声明的名称、参数类型列表、cv 限定符(无)和引用限定符(无)完全相同。 因此,在B中声明的 one 隐藏了A中的那个,并且代码格式不正确,因为重载解析找不到合适的函数来调用。 但是,此处不涉及模板参数列表。

那么应该考虑它们吗? 这就是标准变得不明确的地方。AB被 Clang 视为具有相同的(模板)签名,但 GCC 则不认为。 N.M. 的回答指出,真正的问题实际上在于返回类型。 (确定签名时从不考虑默认模板参数。

请注意,这是在名称查找时决定的。 模板参数推演尚未进行,替换也不进行。 你不能说"哦,扣除/替换失败了,所以让我们去向重载集添加更多成员"。 所以SFINAE在这里没有区别。