【问题标题】:Cannot use pointer to public member function that comes from a private base不能使用指向来自私有基的公共成员函数的指针
【发布时间】:2022-01-15 09:40:19
【问题描述】:

考虑这段代码:

class Base {
 public:
  int foo(int x) const { return 2*x; }
};

class Derived : Base {
 public:
  using Base::foo;
};

现在Derived 有一个公共方法 foo 并且可以调用它

Derived d;
d.foo(2);   // compiles (as it should)

但是,如果我通过指针使用该方法,我将无能为力:

Derived d;
(d.*&Derived::foo)(2);  // does not compile because `Derived::foo` expects a pointer to `Base` and `Derived` cannot be casted to its private base class (without a C-style cast).

对于这种行为是否有任何合乎逻辑的解释,或者可能是标准中的疏忽?

【问题讨论】:

  • using Base::foo;Base 的私有继承一起工作的事实让我感到惊讶。看起来它正在泄漏一个实现细节。
  • 没有疏忽。 using 声明不会改变成员结构。即使通过Derived 访问,它仍然是Base 的成员。
  • 这里引用了这个意思,顺便说一句 stackoverflow.com/q/56619345/817643
  • @FrançoisAndrieux 不,它旨在拉入其他私人继承的成员。您甚至可以引入所有构造函数:using Base::Base;。它不会泄漏实施,IMO。如果您停止从Base 继承,您可以正确实现成员函数。
  • @FrançoisAndrieux “看起来它正在泄露实现细节” 如果Derived 的作者想要泄露实现细节,没有语言机制可以阻止他们这样做。 public: void foo() { Base::foo(); } 做同样的事情。 “正在暴露 Base 和 Derived 之间的关系,否则该关系将被隐藏”该关系没有隐藏,它是不可访问的(就像私有成员没有隐藏但不可访问一样)。

标签: c++ language-lawyer


【解决方案1】:

tl;博士:

  • 声明成员的类是成员函数指针将绑​​定到的类。
  • ->* 上的 Derived 不能与 Base:: 成员函数指针一起使用,除非您可以访问 Derived 中的 private Base(例如在 Derived 的成员函数中或在声明为朋友的函数中Derived)。
  • c 样式转换允许您将 Derived* 转换为 Base* 以及这些类型的成员函数指针,即使 Base 不可访问(这对于任何 c++ 样式转换都是非法的),例如:
    Base* b = (Base*)&d;
    
    在您的示例中是合法的。

1。为什么你会得到一个Base:: 成员函数指针

9.9 The using declaration(强调我的)

12 [注 5:为了在重载决议期间形成一组候选者,派生类中由 using-declaration 命名的函数被视为它们的直接成员派生类。特别是,隐式对象参数被视为对派生类的引用,而不是对基类的引用 ([over.match.funcs])。 这对函数的类型没有影响,并且在所有其他方面,该函数仍然是基类的一部分。 — 尾注]

因此using-声明不会为Derived 创建foo 的版本,但编译器需要假装它是Derived:: 成员以实现重载解析。

因此,在这种情况下,相关位是 这对函数的类型没有影响 - 也就是说,如果您获取 @ 的地址,您仍然会得到 Base:: 的函数指针987654354@.

注意:唯一的例外是构造函数

2。为什么不能将->*Base:: 成员指针一起使用

7.6.4 Pointer-to-member operators(强调我的)

3 二元运算符->* 将其第二个操作数绑定到它的第一个操作数,该操作数的类型应为“T 成员的指针”键入“指向 U 的指针”其中 UTT明确可访问基类。表达式E1->*E2 被转换为等价形式(*(E1)).*E2

这里的问题是你使用foo指针Base不可访问,所以调用不起作用。


3。如何让它工作

实际上要求成员函数可以转换为任何派生类型,只要满足以下几个条件:

7.3.13 Pointer-to-member conversions(强调我的)

2类型为“cv T 类型的 B 成员的指针”类型的纯右值,其中B是一个类类型,可以转换为“指向cvD成员的指针类型的纯右值cv T,其中 D 是从 B 派生的完整类 ([class.derived])。如果 Binaccessible ([class.access])、ambiguous ([class.member.lookup]) 或 virtual (@ 987654329@) D 的基类,或 D 的虚拟基类的基类,需要进行这种转换的程序格式不正确。
[...]

鉴于Base 不像您的示例那样模棱两可或虚拟,我们需要关注的唯一问题是可访问性部分。

我们呢?

其实标准中有一个小漏洞可以利用:

7.6.3 Explicit type conversion (cast notation)(强调我的)

4

执行的转换

可以使用显式类型转换的强制转换表示法来执行。 适用相同的语义限制和行为,但在以下情况下执行static_­cast 时,即使基类不可访问,转换也是有效的:: p>

  • (4.6)指向派生类类型对象或派生类类型左值或右值的指针可以分别显式转换为指向明确基类类型的指针或引用;
  • (4.7)指向派生类类型成员的指针可以显式转换为指向明确的非虚拟基类类型成员的指针;
  • (4.8) 指向明确非虚拟基类类型对象的指针,明确非虚拟基类类型的左值,或 指向明确非虚拟基类类型成员的指针虚拟基类类型可以分别显式转换为指针、引用或指向派生类类型成员的指针

[...]

因此,尽管任何 C++ 转换方法(如 static_cast / reinterpret_cast 等)都不允许从 Base::* 转换为 Derived::*,但允许执行 c 样式转换, 即使基类不可访问

例如:

int main() {
  Derived d;
  auto fn = &Derived::foo;

  // cast to base (only legal with c-style cast)
  // Base* b = static_cast<Base*>(&d); // not legal
  // Base* b = reinterpret_cast<Base*>(&d); // not legal
  Base* b = (Base*)&d; // legal
  (b->*fn)(12);

  // cast member function pointer to derived
  // (also only legal with c-style cast)
  using MemFn = int (Derived::*)(int) const;
  // auto fnD = static_cast<MemFn>(fn); // not legal
  // auto fnD = reinterpret_cast<MemFn>(fn); // not legal
  auto fnD = (MemFn)fn; // legal
  (d.*fnD)(12);

  // or as a one liner (provided by @KamilCuk in the comments):
  // slightly hard to read, but still legal c++:
  (d.*((int(decltype(d)::*)(int))&decltype(d)::foo))(12); // legal
}

godbolt example

是有效的 c++。

所以只需将Derived 转换为Base 或将成员函数指针转换为绑定到Derived 的指针。

标准中甚至有一个例子可以做到这一点:11.8.3 Accessibility of base classes and base class members (3)


4。为什么&amp;Derived::foo 不返回Derived::* memfn 指针?

因为标准是这样说的。我不知道他们为什么会这样决定,但我可以推测潜在的原因可能是什么:

  • 您可以检查哪个派生最多的类实现了给定的成员函数。如果&amp;Derived::foo 返回Derived::* 指针,这将中断。 (例如,这可以与 CRTP 一起使用来检查给定的Derived 类是否为Base 的给定成员提供了新定义) 例如:

    class Base {
    public:
        int foo(int x) const { return 2*x; }
    };
    
    class Derived : private Base {
    public:
        using Base::foo;
    };
    
    template<class T>
    struct implementing_class_helper;
    
    template<class T, class R>
    struct implementing_class_helper<R T::*> {
        typedef T type;
    };
    
    template<class T>
    struct implementing_class : implementing_class_helper<typename std::remove_cv<T>::type> {
    
    };
    
    template<class T>
    using implementing_class_t = implementing_class<T>;
    
    int main() {
      static_assert(std::is_same_v<
        typename implementing_class<decltype(&Derived::foo)>::type,
        Base
      >, "Shenanigans!");
    }
    
  • 如果要创建存根函数,例如:

    class Base {
    public:
      int foo(int x) const { return 2*x; }
    };
    
    class Derived : Base {
    public:
      // pretending using Base::foo; would result in this:
      int foo(int x) { return Bar::foo(x); }
    };
    

    编译器现在会出现问题,因为Base::fooDerived::foo 是不同的函数,但仍需要比较等于Base::foo,因为这是实际的实现。
    因此,编译器需要知道所有编译单元中包含using Base::foo; 的所有类,并确保无论何时将它们的::fooBase::foo 进行比较,结果都是true。对于一个奇怪的边缘情况,这听起来像是很多实现工作。

【讨论】:

  • (d.*((int(decltype(d)::*)(int))&amp;decltype(d)::foo))(12); 好笑
  • @KamilCuk 绝对应该因为默默无闻而获得奖励 :D 但它仍然是合法的 c++ :)
猜你喜欢
  • 2013-06-14
  • 2012-05-11
  • 2011-10-20
  • 2023-03-27
  • 1970-01-01
  • 2018-03-24
  • 2010-12-14
  • 1970-01-01
  • 1970-01-01
相关资源
最近更新 更多