以下代码无法在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)();
}
Run Code Online (Sandbox Code Playgroud)
这是否符合标准?公共使用声明是否应该B允许B::f透明地使用成员函数指针,而不是A::f在B透视之外涉及可访问性?
是的,你的程序是不正确的.
C++ 17(N4659)[namespace.udecl]/16(强调我的):
出于重载解析的目的,将using声明引入派生类的函数视为派生类的成员.特别是,隐式
this参数应被视为指向派生类而不是基类的指针.这对函数的类型没有影响,并且在所有其他方面,函数仍然是基类的成员.
换句话说,using-declaration不添加成员B,只是为同一成员添加第二个名称A::f.可以通过名称查找来选择第二个名称,并将其用于对名称使用的可访问性检查,但除此之外,除了重载解析之外,它等同于原始成员.
[expr.unary.op]/3:
一元运算
&符的结果是指向其操作数的指针.操作数应为左值或限定ID.如果操作数是一个合格-ID命名非静态或变体构件m某些类的C类型T,结果类型"指针类的成员C类型的T",并且是一个指定prvalueC::m.
因此,即使合格的-ID B::f您使用的是拼写的名字class B和合格的名称查找发现通过引入的名称using声明中B,实际的函数表达式名称是的成员A,所以表达&B::f的类型为"指针A()函数类的函数返回void",或者作为type-id , void (A::*)(). 您可以通过添加到您的示例来验证这一点:
#include <type_traits>
static_assert(std::is_same<decltype(&B::f), void (A::*)()>::value, "error");
Run Code Online (Sandbox Code Playgroud)
最后,在[conv.mem]/2中:
类型为" cv
B类型成员的指针"的prvalue ,其中是类类型,可以转换为类型为"指向cv类型成员的指针"的prvalue ,其中是一个派生类.如果是不可访问,模糊或虚拟基类,或虚拟基类的基类,则需要这种转换的程序是不正确的.TBDTDBBDD
因此,将指向成员函数的指针命名为有效,但是将其转换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;
};
Run Code Online (Sandbox Code Playgroud)
或者,如果你真的必须,使用C风格的演员.在某些情况下,允许C样式转换忽略类继承关系的可访问性,其中没有任何C++样式的转换可以使用相同的结果进行编译.(reinterpret_cast也可以将任何指向成员函数的指针转换为任何其他指针,但其结果未指定.)
int main()
{
B b;
void (B::*f)() = (void (B::*)()) &B::f;
(b.*f)();
}
Run Code Online (Sandbox Code Playgroud)
请注意问题附带的评论:如果您更改main为
int main()
{
B b;
auto f = &B::f;
(b.*f)();
}
Run Code Online (Sandbox Code Playgroud)
然后的类型f是void (A::*)()如以上所解释.但是你遇到[expr.mptr.oper]/2(再次强调我的):
二元运算符
.*将其第二个操作数绑定到第一个操作数,该操作数应为"指向成员的指针T"类型,该第一个操作数应为类的glvalueT或其类T是明确且可访问的基类.
因此,您仍然存在成员函数与其原始类相关联的问题,并且不能被视为成员,B除非在B任何朋友的范围内.