提问人:Toboxos 提问时间:5/4/2022 最后编辑:dfribToboxos 更新时间:5/4/2022 访问量:276
使用父类方法作为派生类方法时出现 GCC 错误
GCC error when using parent class method as derived class method
问:
我的代码中有一个函数,它只接受类成员方法作为模板参数。我需要使用从父类继承的类方法来调用此方法。这是我的问题的示例代码:
template <class C>
class Test {
public:
template<typename R, R( C::* TMethod )()> // only a member function should be accepted here
void test() {}
};
class A {
public:
int a() { return 0; } // dummy method for inheritance
};
class B : public A {
public:
using A::a; // A::a should be declared in the B class declaration region
// int a() { return A::a(); } // if this lines is activated compliation works
};
int main() {
auto t = Test<B>();
t.test<int, &B::a>();
}
使用 MSVC 2019 编译器,代码可以毫无问题地编译。但是,gcc 会产生以下错误:
<source>: In function 'int main()':
<source>:23:23: error: no matching function for call to 'Test<B>::test<int, &A::a>()'
23 | t.test<int, &B::a>();
| ~~~~~~~~~~~~~~~~~~^~
<source>:5:10: note: candidate: 'template<class R, R (B::* TMethod)()> void Test<C>::test() [with R (C::* TMethod)() = R; C = B]'
5 | void test() {}
| ^~~~
<source>:5:10: note: template argument deduction/substitution failed:
<source>:23:17: error: could not convert template argument '&A::a' from 'int (A::*)()' to 'int (B::*)()'
23 | t.test<int, &B::a>();
|
据我了解,gcc 仍在将 B::a 的类型处理为 A::a。在 cpp 参考上,它说使用
将其他位置定义的名称引入到声明性中 显示此 using-declaration 的区域。
因此,在我看来,使用应该将 A::a 方法转移到 B 的 declerativ 区域,因此它应该作为 B::a 处理。 是我错了还是 GCC 中存在错误?
下面是编译器资源管理器上的示例: https://godbolt.org/z/TTrd189sW
答:
在转换后的常量表达式中不允许进行(非 nullptr)指针到成员的转换
所以在我看来,使用应该将 A::a 方法转移到 B 的递减区域,因此它应该被处理为 B::a。 是我错了还是 GCC 中存在错误?
你错了,但我们需要深入研究语言规则的兔子洞才能找出原因。
首先,指向成员的指针类型,即使通过派生类引用(即使通过 using 声明引入)也是指向基成员的指针类型。[expr.unary.op]/3 的(非规范性)示例明确涵盖了以下用例:
一元 & 运算符的结果是指向其操作数的指针。
- (3.1) 如果操作数是 qualified-id,以命名某个类 C 的非静态或变体成员 m,类型为 T,则结果具有 type “指向类型 T 的类 C 成员的指针”,并且是 prvalue 指定 C::m。
- [...]
[示例 1:
struct A { int i; }; struct B : A { }; ... &B::i ... // has type int A::* <-- !!! int a; int* p1 = &a; int* p2 = p1 + 1; // defined behavior bool b = p2 > p1; // defined behavior, with value true
— 结束示例]
但是,您可以将 [conv.mem]/2 转换为 (base) 的 [conv.mem]/2 涵盖:int (A::*)()
int (B::*)()
类型为“指向 cv T 类型 B 成员的指针”的 prvalue(其中 B 是类类型)可以转换为类型为“指向 cv T 类型 D 成员的指针”的 prvalue,其中 D 是从 B 派生的完整类 ([class.derived])。如果 B 是 D 的不可访问 ([class.access])、不明确的 ([class.member.lookup]) 或虚拟 ([class.mi]) 基类,或者是 D 的虚拟基类的基类,则需要此转换的程序格式不正确。转换的结果引用的成员与转换发生之前指向成员的指针相同,但它引用基类成员,就好像它是派生类的成员一样。结果引用 D 的 B 实例中的成员。由于结果的类型为“指向 cv T 型 D 成员的指针”,因此使用 D 对象间接通过它是有效的。结果与通过指针指向具有 D 的 B 子对象的 B 成员相同。空成员指针值将转换为目标类型的空成员指针值。
也就是说,指向基类成员的指针可以转换为派生类的成员,实际上,以下程序在参数(指向基成员的指针)到函数参数(类型:派生成员的指针)的上下文中进行转换的格式正确:
struct A {
int a() { return 0; };
};
struct B : A {};
void f(int( B::*)()) {}
int main() {
f(&A::a); // OK: [conv.mem]/2
}
那为什么模板参数的情况会失败呢?一个更简单的例子是:
struct A {
int a() { return 0; };
};
struct B : A {};
template<int(B::* TMethod )()>
void g() {}
int main() {
g<&A::a>(); // error
}
根本原因是模板参数推导失败:模板参数为 type,并且 [temp.arg.nontype]/2 适用:&A::a
int(A::*)()
非类型模板参数的模板参数应是模板参数类型的转换常量表达式 ([expr.const])。
在转换后的常量表达式(请参阅 [expr.const]/10)中不允许使用(非 nullptr)指针到成员的转换 ([conv.mem]/2),这意味着不是 type 的非类型模板参数的有效模板参数。&A::a
int(B::*)()
我们可能会注意到,如果我们改用类模板,Clang 实际上给了我们一个非常清晰的诊断:
struct A {
int a() { return 0; };
};
struct B : A {};
template<int(B::*)()>
struct C {};
int main() {
C<&A::a> c{};
// error: conversion from 'int (A::*)()' to 'int (B::*)()'
// is not allowed in a converted constant expression
}
评论
int (A::*p)(); p= &B::a;
有 namespace.udecl,第 12 项(强调我的):
为了在超载期间形成一组候选者 resolution,由派生的 using 声明命名的函数 类被视为派生的直接成员 类。这对函数的类型没有影响, 在所有其他方面,该函数仍然是基类的一部分。
因此,不是 的成员,而 的类型是 。
(不管你是否包括,意思都是一样的)a
B
&B::a
int (A::*)()
&B::a
using A::a;
基类中的命名函数没有任何意义,除非在要重载或重写它们时解决“隐藏问题”。using
评论
Error (active) E0304 no function template instance "Test<C>::test [with C=B]" matches argument list
B
B
A
using A::a;
A::a
B
B::a
A::a