使用父类方法作为派生类方法时出现 GCC 错误

GCC error when using parent class method as derived class method

提问人:Toboxos 提问时间:5/4/2022 最后编辑:dfribToboxos 更新时间:5/4/2022 访问量:276

问:

我的代码中有一个函数,它只接受类成员方法作为模板参数。我需要使用从父类继承的类方法来调用此方法。这是我的问题的示例代码:

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

使用 using 指令的 c++ gcc language-lawyer

评论

1赞 Some programmer dude 5/4/2022
应该注意的是,Clang 也无法编译代码(错误消息要少得多)。
0赞 anastaciu 5/4/2022
“使用 MSVC 2019 编译器,代码编译没有问题” - godbolt 上的 MSVC 版本编译没有问题,但我的本地编译器 19.29.30143 确实发出了错误:.Error (active) E0304 no function template instance "Test<C>::test [with C=B]" matches argument list
1赞 molbdnilo 5/4/2022
请注意,这不是查找问题,而是类型问题。名称 “a” 在 中引入,但该函数不会成为 的成员。它的类型保持不变,因为它引用了 的成员。BBA
2赞 molbdnilo 5/4/2022
也就是说,并不意味着“也成为成员”,而是表示“用作”的同义词”。using A::a;A::aBB::aA::a
0赞 Toboxos 5/4/2022
这是有道理的。您是否对 C++ 标准有任何其他参考来解释这一点?

答:

0赞 dfrib 5/4/2022 #1

在转换后的常量表达式中不允许进行(非 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::aint(A::*)()

非类型模板参数的模板参数应是模板参数类型的转换常量表达式 ([expr.const])。

在转换后的常量表达式(请参阅 [expr.const]/10)中不允许使用(非 nullptr)指针到成员的转换 ([conv.mem]/2),这意味着不是 type 的非类型模板参数的有效模板参数。&A::aint(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
}

评论

1赞 Sam Varshavchik 5/4/2022
这似乎没有抓住重点。如果编译得很好(它应该),那么我看不出模板有什么问题。int (A::*p)(); p= &B::a;
0赞 dfrib 5/4/2022
@SamVarshavchik 更新了更多标准引用:我认为这里的关键是指向成员的指针转换(根据 [conv.mem])不是转换后的常量表达式,这意味着 [temp.arg.nontype]/2 不适用,并且模板参数推导失败。
1赞 molbdnilo 5/4/2022 #2

namespace.udecl,第 12 项(强调我的):

为了在超载期间形成一组候选者 resolution,由派生的 using 声明命名的函数 类被视为派生的直接成员 类。这对函数的类型没有影响, 在所有其他方面,该函数仍然是基类的一部分

因此,不是 的成员,而 的类型是 。
(不管你是否包括,意思都是一样的)
aB&B::aint (A::*)()&B::ausing A::a;

基类中的命名函数没有任何意义,除非在要重载或重写它们时解决“隐藏问题”。using