Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Cannot use pointer to public member function that comes from a private base

Consider this code:

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

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

Now Derived has a public method foo and it can be called

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

However, I cannot do anything if I use the method through a pointer:

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).

Is there any logical explanation for this behaviour or perhaps it is an oversight in the standard?

like image 272
antonpp Avatar asked Dec 10 '21 18:12

antonpp


1 Answers

tl;dr:

  • the class within which the member is declared is the class that a member function pointer will bind to.
  • ->* on a Derived doesn't work with a Base:: member function pointer unless the private Base in Derived is accessible to you (e.g. within a member function of Derived or in a function declared as friend of Derived).
  • c-style casts allow you to convert Derived* to Base* as well as member function pointers of those types, even though Base is not accessible (this would be illegal for any c++-style cast), e.g.:
    Base* b = (Base*)&d;
    
    would be legal in your example.

1. Why you get a Base:: member function pointer

9.9 The using declaration (emphasis mine)

12 [Note 5: 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. In particular, the implicit object parameter is treated as if it were a reference to the derived class rather than to the base class ([over.match.funcs]). This has no effect on the type of the function, and in all other respects the function remains part of the base class. — end note]

So the using-declaration will not create a version of foo for Derived, but the compiler needs to pretend like it would be a Derived:: member for the purpose of overload resolution.

So the relevant bit in this case is This has no effect on the type of the function - i.e. you'll still get a function-pointer for Base:: if you take the address of foo.

Note: The only exception to this is for constructors

2. Why you can't use ->* with the Base:: member pointer

7.6.4 Pointer-to-member operators (emphasis mine)

3 The binary operator ->* binds its second operand, which shall be of type “pointer to member of T” to its first operand, which shall be of type “pointer to Uwhere U is either T or a class of which T is an unambiguous and accessible base class. The expression E1->*E2 is converted into the equivalent form (*(E1)).*E2.

The problem here is that at the point you use the foo pointer Base is not accessible, so the call doesn't work.


3. How to make it work

Member functions are actually required to be castable to any derived type, as long as a few criteria are met:

7.3.13 Pointer-to-member conversions (emphasis mine)

2A 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.
[...]

Given that Base is neither ambiguous or virtual like in your example, the only problem we need to focus on is the accessibility part.

Or do we?

There's actually a small loophole in the standard that we can use:

7.6.3 Explicit type conversion (cast notation) (emphasis mine)

4 The conversions performed by

  • (4.1) a const_­cast ([expr.const.cast]),
  • (4.2) a static_­cast ([expr.static.cast]),
  • (4.3) a static_­cast followed by a const_­cast,
  • (4.4) a reinterpret_­cast ([expr.reinterpret.cast]), or
  • (4.5) a reinterpret_­cast followed by a const_­cast,

can be performed using the cast notation of explicit type conversion. The same semantic restrictions and behaviors apply, with the exception that in performing a static_­cast in the following situations the conversion is valid even if the base class is inaccessible:

  • (4.6) a pointer to an object of derived class type or an lvalue or rvalue of derived class type may be explicitly converted to a pointer or reference to an unambiguous base class type, respectively;
  • (4.7) a pointer to member of derived class type may be explicitly converted to a pointer to member of an unambiguous non-virtual base class type;
  • (4.8) a pointer to an object of an unambiguous non-virtual base class type, a glvalue of an unambiguous non-virtual base class type, or a pointer to member of an unambiguous non-virtual base class type may be explicitly converted to a pointer, a reference, or a pointer to member of a derived class type, respectively.

[...]

So while any C++-casting method (like static_cast / reinterpret_cast, etc...) doesn't allow the conversion from Base::* to Derived::*, a c-style cast is allowed to perform it, even when the base-class is inaccessible.

e.g.:

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

is valid c++.

So just cast the Derived to Base or the member function pointer to one that's bound to Derived.

There's even an example in the standard that does exactly that: 11.8.3 Accessibility of base classes and base class members (3)


4. Why doesn't &Derived::foo return a Derived::* memfn pointer?

Because the standard says so. I don't know why they decided this way, but i can speculate what the potential reasons might be:

  • You can check which most-derived class implemented a given member function. This would break if &Derived::foo would return a Derived::* pointer. (This can e.g. be used with CRTP to check if a given Derived class has provided a new definition for a given member of Base) e.g.:

    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!");
    }
    
  • If a stub function would be created, e.g.:

    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); }
    };
    

    The compiler would now have a problem, since Base::foo and Derived::foo are different functions, but still need to compare equal to Base::foo, since that's the actual implementation.
    So the compiler would need to know all classes that contain using Base::foo; in all compilation units and make sure whenever you compare their ::foo with Base::foo that the result is true. And that sounds like a lot of implementation work for a strange edge-case.

like image 172
Turtlefight Avatar answered Oct 18 '22 19:10

Turtlefight