获取指向从私有基类别名的成员函数的指针

Taking a pointer to a member function aliased from a private base class

本文关键字:别名 成员 函数 指针 基类 取指 获取      更新时间:2023-10-16

以下代码无法在Linux上使用GCC 7.2.0和Clang 5.0.0进行编译。

#include <iostream>
struct A
{
void f()
{
std::cout << "Hello, world!n";
}
};
struct B : private A
{
using A::f;
};
int main()
{
B b;
void (B::*f)() = &B::f; // Error: 'A' is an inaccessible base of 'B'
(b.*f)();
}

这符合标准吗?B中的公共使用声明不应该允许透明地获取指向B::f的成员函数指针,而不是在B的视角之外涉及A::f的可访问性吗?

是的,您的程序格式不正确。

C++17 (N4659) [namespace.udecl]/16 (强调我的):

出于重载解析的目的,将using-声明引入派生类的函数视为派生类的成员。特别是,应将隐式this参数视为指向派生类而不是基类的指针。这对函数的类型没有影响,并且在所有其他方面,函数仍然是基类的成员。

换句话说,using-声明不会添加B的成员,它只是为同一成员A::f添加第二个名称。 可以通过名称查找选择第二个名称,并用于对名称的使用进行辅助功能检查,但除此之外,除了重载解析之外,它等效于原始成员。

[expr.unary.op]/3:

一元运算符的结果&是指向其操作数的指针。操作数应为左值或限定 ID。如果操作数是一个限定 id,命名某个类C的非静态或变体成员m类型为T,则结果的类型为"指向类型T的类C成员的指针",并且是指定C::m的 prvalue 。

因此,即使您使用的限定 idB::f是用class B的名称拼写的,并且限定名查找在B中找到了 using 声明引入的名称,表达式名称的实际函数是A的成员,因此表达式&B::f的类型为 "指向类A成员的指针,其类型为 () 返回void", 或作为类型 IDvoid (A::*)(). 您可以通过添加到示例中来验证这一点:

#include <type_traits>
static_assert(std::is_same<decltype(&B::f), void (A::*)()>::value, "error");

最后,在 [conv.mem]/2 中:

类型为"指向cvT类型B的成员的指针"的 prvalue,其中B是类类型,可以转换为类型为 "指向cvT类型的D成员的指针"的 prvalue ,其中DB的派生类。如果B是不可访问的、不明确的或D的虚拟基类,或者是D的虚拟基类的基类,那么需要这种转换的程序是格式不正确的。

因此,命名指向成员函数的指针是有效的,但将其从void (A::*)()转换为void (B::*)()则无效,因为无法从main访问继承。

作为一种解决方法,除了成员函数本身之外,您还可以在B中提供对指向成员函数的指针的访问:

struct B : private A
{
using A::f;
using func_ptr_type = void (B::*)();
static constexpr func_ptr_type f_ptr = &A::f;
};

或者,如果您真的必须,请使用 C 型演员表。 在某些情况下,允许 C 样式强制转换忽略类继承关系的可访问性,其中任何C++样式强制转换都无法使用相同的结果进行编译。 (reinterpret_cast还可以将指向成员函数的任何指针转换为任何其他指针,但其结果未指定。

int main()
{
B b;
void (B::*f)() = (void (B::*)()) &B::f;
(b.*f)();
}

关于问题所附评论的说明:如果将main更改为

int main()
{
B b;
auto f = &B::f;
(b.*f)();
}

然后如上所述void (A::*)()f的类型。 但是后来你遇到了[expr.mptr.oper]/2(再次强调我的):

二元运算符.*将其第二个操作数(类型为"指向T成员的指针")绑定到其第一个操作数,该操作数应是类T的 glvalue 或T是明确且可访问的基类的类的 glvalue。

因此,您仍然会遇到一个问题,即成员函数与其原始类相关联,并且不能被视为B的成员,除非在B和任何朋友的范围内。