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

GCC error when using parent class method as derived class method

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

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

Introduces a name that is defined elsewhere into the declarative region where this using-declaration appears.

所以在我看来,using应该将A::a方法转移到B的declerativ区域,因此它应该被处理为B::a。我错了还是 GCC 有错误?

这是编译器资源管理器上的示例:https://godbolt.org/z/TTrd189sW

(Non-nullptr) pointer-to-member 转换常量表达式中不允许转换

So in my opinion the using should transfer the A::a method to the declerative region of B and therefor it should be handled as B::a. Am I wrong or is there a bug in GCC?

你错了,但我们需要稍微深入一下语言规则的兔子洞才能找出原因。

首先,指向成员的指针的 type,即使通过派生的 class 引用(即使通过 using 声明引入)也是如此pointer-to-member 的基础。 [expr.unary.op]/3 的 (non-normative) 示例明确涵盖了这个用例:

The result of the unary & operator is a pointer to its operand.

  • (3.1) If the operand is a qualified-id naming a non-static or variant member m of some class C with type T, the result has type “pointer to member of class C of type T” and is a prvalue designating C​::​m.
  • [...]

[Example 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

— end example]

但是 [conv.mem]/2 涵盖了您可以将 int (A::*)()(基础)转换为 int (B::*)()(派生):

A prvalue of type “pointer to member of B of type cv T”, where B is a class type, can be converted to a prvalue of type “pointer to member of D of type cv T”, where D is a complete class derived ([class.derived]) from B. If B is an inaccessible ([class.access]), ambiguous ([class.member.lookup]), or virtual ([class.mi]) base class of D, or a base class of a virtual base class of D, a program that necessitates this conversion is ill-formed. The result of the conversion refers to the same member as the pointer to member before the conversion took place, but it refers to the base class member as if it were a member of the derived class. The result refers to the member in D's instance of B. Since the result has type “pointer to member of D of type cv T”, indirection through it with a D object is valid. The result is the same as if indirecting through the pointer to member of B with the B subobject of D. The null member pointer value is converted to the null member pointer value of the destination type.

也就是说,指向基 class 成员的指针可以转换为派生 class 的成员,事实上,下面的程序,其中转换是在参数的上下文中进行的 ( pointer-to-member of base) 到函数参数(类型:pointer-to-member of derived)是 well-formed:

struct A {
    int a() { return 0; };
};

struct B : A {};

void f(int( B::*)()) {}

int main() {
    f(&A::a);  // OK: [conv.mem]/2
}

那为什么模板参数的case会失败呢?一个更小的例子是:

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适用:

A template-argument for a non-type template-parameter shall be a converted constant expression ([expr.const]) of the type of the template-parameter.

A (non-nullptr) pointer-to-member conversion ([conv.mem]/2) 不允许在转换后的常量表达式中(参考[expr.const]/10),意思是&A::a 不是类型为 int(B::*)().

的 non-type 模板参数的有效模板参数

我们可能会注意到,如果我们更改为 class 模板,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
}

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

For the purpose of forming a set of candidates during overload resolution, the functions named by a using-declaration in a derived class are treated as though they were direct members of the derived class. [...] This has no effect on the type of the function, and in all other respects the function remains part of the base class.

因此,a不是B的成员,&B::a的类型是int (A::*)()
&B::a 意思是一样的,不管你是否包含 using A::a;

using 来自基础 class 的命名函数毫无意义,除非在您想要重载或覆盖它们时解决“隐藏问题”。