19

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?

W.F.
  • 13,888
  • 2
  • 34
  • 81
antonpp
  • 2,333
  • 23
  • 28
  • 3
    The fact that `using Base::foo;` works with private inheritance of `Base` is the part that surprises me. It looks like it is leaking an implementation detail. – François Andrieux Dec 10 '21 at 18:29
  • 2
    No oversight. The using declaration doesn't change the membership structure. It's still a member of `Base` being named, even through the `Derived` access. – StoryTeller - Unslander Monica Dec 10 '21 at 19:02
  • Here's a quote to that effect, btw https://stackoverflow.com/q/56619345/817643 – StoryTeller - Unslander Monica Dec 10 '21 at 19:07
  • @FrançoisAndrieux No, it is intended to pull in otherwise privately inherited members. You can even pull in all constructors: `using Base::Base;`. It doesn't leak implementation, IMO. If you stop inheriting from `Base` you can implement the member function proper. – bitmask Dec 10 '21 at 19:11
  • @bitmask `private` inheritance goes beyond making all base members inherited privately. Comparatively to `public` inheritance, it does not allow references to the derived type to be converted to references to the base type. It doesn't not define the "is a" relationship. It looks to me like `using Base::foo;` is exposing the relationship between `Base` and `Derived` which would otherwise be hidden. Normally you should be able to change `Base` to another compatible type with no external change, but `&Derived::foo` is leaking the type `Base`, potentially making such a change a breaking change. – François Andrieux Dec 10 '21 at 19:15
  • @FrançoisAndrieux I don't think it does. `using Base::foo` is a shorthand for writing a wrapper function, having to forward all the arguments. It is therefore odd that `foo` doesn't appear to be considered a `Derived` member afterwards for the purposes of taking a member function pointer. – bitmask Dec 10 '21 at 19:23
  • @bitmask `using` imports an identifier for resolution, it doesn't define an equivalent member, except for importing constructors which works under different rules. It is not shorthand for writing a wrapper. – François Andrieux Dec 10 '21 at 19:33
  • It isn't shorthand for writing a wrapper, but from a design perspective, I see almost no difference between a) writing a wrapper and b) importing the identifier for resolution... other than the fact that a wrapper function wouldn't exhibit the behavior central to this question because it is explicitly treated as a member of the derived class in all cases. This is why **many** people talk about private inheritance as "just another way" to express has-a relationships, similar to composition. Can you provide a practical difference between a) and b)? – Alexander Guyer Dec 10 '21 at 19:41
  • 1
    @FrançoisAndrieux "It looks like it is leaking an implementation detail" If authors of `Derived` want to leak implementation details, there is no language mechanism to prevent them from doing so. `public: void foo() { Base::foo(); }` does the same thing. "is exposing the relationship between Base and Derived which would otherwise be hidden" The relationship is not hidden, it is inaccessible (just like private members are not hidden but inaccessible). – n. m. could be an AI Dec 10 '21 at 19:57
  • @AlexanderGuyer I'm not sure who you intended to reply to, but if your comment was directed at me, I don't claim there is a significant difference, other than the technicality I describe. I was trying to explain why that technicality matters in the context of this question. – François Andrieux Dec 10 '21 at 20:00
  • @n.1.8e9-where's-my-sharem. I do not understand the distinction you are making. It seems to me making the relationship externally inaccessible is the means by which the relationship is hidden. Maybe you interpreted that I was speaking of the principal identifier hiding (where one identifier dominates another identical identifier) but that is not the case. And what I mean by "it leaks implementation detail" is that it can accidentally providing information that was probably intended to be `private`. It isn't immediately obvious that it exposes `Base` and it does not look intentional. – François Andrieux Dec 10 '21 at 20:05
  • @FrançoisAndrieux The difference between "hidden" and "inaccessible" can be illustrated by [this example](https://stackoverflow.com/questions/70306045). `private` does not hide things by design. "It isn't immediately obvious that it exposes Base and it does not look intentional" There is no other reason to do this except to expose `Base::foo`. Looks totally intentional to me. – n. m. could be an AI Dec 10 '21 at 20:10
  • @n.1.8e9-where's-my-sharem. Thank you for that reply. Regarding the distinction between hidden and inaccessible, I see what you mean now. But I don't think the same distinction applies to inheritance. If you have an example of that, I would be interested in seeing that. Regarding intentionally exposing `Base::foo`, I also now see what you mean. Though what I think is unintentional is that most developers will think that `using Base::foo` defines a member for `Derived` and they would not realize it exposes the type `Base`. If that is intentional, then OK. But I bet it was not intended here. – François Andrieux Dec 10 '21 at 20:18
  • @n.1.8e9-where's-my-sharem. To clarify about an example based on inheritance, I mean an example where `private` base type inheritance can be differentiated from not inheriting from the base type at all, for an external observer. – François Andrieux Dec 10 '21 at 20:19
  • @FrançoisAndrieux it says `Base::foo` right there, what else could it possibly do? "an example where private base type inheritance can be differentiated from not inheriting from the base type at all" Not sure why the situation with private bases should be different from the situation with private members. Do you think [this](https://godbolt.org/z/dYvoTr6PK) should compile? If private base classes are truly invisible, this looks like a perfectly valid program. But in reality it isn't. – n. m. could be an AI Dec 10 '21 at 20:40
  • @n.1.8e9-where's-my-sharem. Yes, I thought that would have compile. Thanks for the counter example, I guess there is a distinction I didn't understand. About `Base::foo` explicitly exposing `Base`, it does seem obvious when you put it that way. But I maintain that in the code shown, the intention was most likely to forward `foo` and to avoid writing a wrapper, and that exposing `Base` seems to be an accidental consequence of the chosen solution. – François Andrieux Dec 10 '21 at 20:57
  • @FrançoisAndrieux The problem in that case is that C++ does overload resolution before it actually checks if the conversion required to call the functions is actually possible. [related question](https://stackoverflow.com/questions/6239630/trouble-with-const-non-const-overload-resolution) - so in this case you get the error that the call is ambigous before the compiler even checked if conversion to `A&` would actually be legal. - also you can always get a reference/pointer to a private base class with a c-style cast, e.g. `Base* b = (Base*)&d;` would be legal in the OP's case. – Turtlefight Dec 11 '21 at 00:33

1 Answers1

13

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)

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

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

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.

Turtlefight
  • 9,420
  • 2
  • 23
  • 40