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

7

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

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 出现的声明区域中。

因此,在我看来,using 应该将 A::a 方法转移到 B 的声明区域中,因此应将其处理为 B::a。我错了吗?还是 GCC 中有 bug?

以下是 Compiler Explorer 上的示例:https://godbolt.org/z/TTrd189sW


1
需要注意的是,Clang编译代码也失败了(并且提供的错误信息不太可用)。 - Some programmer dude
使用MSVC 2019编译器,代码可以顺利编译。但是在我的本地编译器19.29.30143中会出现错误:Error (active) E0304 no function template instance "Test<C>::test [with C=B]" matches argument list。在godbolt上的MSVC版本没有问题。 - anastaciu
1
请注意,这不是查找问题,而是类型问题。在B中引入了name“a”,但该函数并未成为B的成员。它的type保持不变,因为它仍然指向A的成员。 - molbdnilo
2
也就是说,using A::a;并不意味着“将A::a也作为B的成员”,而是表示“将B::a作为A::a的同义词使用”。 - molbdnilo
这是有道理的。你有关于C++标准的其他参考资料,可以解释这个问题吗? - Toboxos
2个回答

1

有一个namespace.udecl,第12项(我强调):

为了在重载解析期间形成一组候选函数,在派生类中使用声明的函数被视为直接成员,就好像它们是派生类的直接成员一样。[...] 这对函数的类型没有影响,在其他方面,该函数仍然是基类的一部分

因此,a不是B的成员,&B::a的类型是int (A::*)()
(无论是否包含using A::a;&B::a的含义都相同)

除了解决“隐藏问题”以便重载或覆盖之外,从基类中使用命名函数没有任何意义。


0

(非 nullptr)成员指针转换在转换常量表达式中不允许

所以我认为使用应该将 A::a 方法传递到 B 的声明区域,因此它应该被处理为 B::a。我错了还是 GCC 中有 bug?

你错了,但我们需要深入了解语言规则才能找出原因。

首先,即使通过派生类引用(即使通过 using 声明引入),成员指针的类型仍然是基类的成员指针。[expr.unary.op]/3 的(非规范性)示例明确涵盖了这种情况:

一元运算符 & 的结果是其操作数的指针。 (3.1)如果操作数是限定符标识符,命名某个类 C 的非静态或变体成员 m,类型为 T,则结果具有类型“类 C 的成员指针,类型为 T”,并且是一个 prvalue,指代 C::m。 [...] [示例 1: 结构体 A {int i; }; 结构体 B:A {}; ...&B::i ... // 类型为 int A:: * < - !!! int a; int * p1 = &a; int * p2 = p1 + 1; // 定义行为 bool b = p2> p1; // 定义行为,值为 true — 结束示例]

然而[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的虚拟基类的基类,则需要进行此转换的程序是非法的。转换的结果引用与转换之前的成员指针相同的成员,但它将基类成员引用为派生类的成员。结果引用B的实例中的成员D。由于结果具有类型“指向类型为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
}

根本原因在于模板参数推导失败:模板参数是类型为int(A::*)()&A::a,并且[temp.arg.nontype]/2适用:

非类型模板参数的模板参数必须是转换后的常量表达式([expr.const])与模板参数的类型相同。

转换后的常量表达式中不允许使用(非nullptr)成员指针转换([conv.mem]/2)(请参阅[expr.const]/10),这意味着&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
}

1
这似乎没有抓住重点。如果 int (A::*p)(); p= &B::a; 编译正常(应该是这样的),那么我不明白模板有什么问题。 - Sam Varshavchik
@SamVarshavchik 更新了几个标准参考文献:我认为关键在于成员指针转换(根据[conv.mem])不是转换后的常量表达式,这意味着[temp.arg.nontype]/2不适用,模板参数推导失败。 - dfrib

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接