ROX
ROX

Reputation: 1266

C++ public virtual inhertiance of interface with private inheritance of implementation

My question is about why the following code behaves as it does. I'm not asking about the quality of the design (I know some of you will immediately hate the multiple inheritance and I'm not arguing for or against it here) I could ask a separate question that goes into what the author was trying to achieve, but lets just assume there is code that is equivalent to this:-

class IReadableData
{
    public: virtual int getX() const = 0;
};

class Data : public virtual IReadableData
{
   public: 
      virtual int getX() const { return m_x; }
      void setX(int x) {m_x = x;}
   private:
      int m_x;   
};

class ReadableData : private Data, public virtual IReadableData
{
     // I'd expected to need a using here to expose Data::getX
};

This complies on visual studio 2017 with "warning C4250: 'ReadableData': inherits 'Data::Data::getX' via dominance"

Firstly I was a bit surprised not to be told that ReadableData didn't implement getX (given its implementation is private), but no warning occurs and I can create a ReadableData, but although ReadableData publicly inherits from IReadableData, the public methods of IReadableData are inaccessible

    ReadableData r;
    //  r.getX(); error C2247: 'Data::getX' not accessible because 'ReadableData' uses 'private' to inherit from 'Data'

However the following does compile

    ReadableData r;
    IReadableData& r2 = r;
    r2.getX();

This seems inconsistent to me either r is an IReadableData and getX should be available or it isn't and the assignment to r2 (or the definition of ReadableData) should fail. Should this happen? what in the standard leads to this?

This question:- Diamond inheritance with mixed inheritance modifers (protected / private / public)

seems connected, except that the base in that case is not abstract, and its answer citing section 11.6 would make me think that r.getX(); should have been accessible.

Edit: I made example code slightly less minimal to give a tiny bit of insight into intention, but it doesn't change the question really.

Upvotes: 3

Views: 463

Answers (2)

It's a derivative of the lookup rules, which have some virtual inheritance related bullet points. The idea is that virtual inheritance should not cause ambiguity during name lookup. So a different declaration of getX() is found in each scenario. Since access specifiers are only checked after name lookup (and do not affect name resolution) you can hit such snags.

  • In the case of r2.getX();, lookup begins in the context of IReadableData. Since a declaration is found immediately, and IReadableData has no bases, this is what the lookup resolves to. It's a public member of IReadableData, so you can name and call it. After that, it's the dynamic dispatch mechanism that takes care of calling the implementation given by the dominating Data::getX().

  • In the case of r.getX();, lookup works differently. It starts in the context of ReadableData. There is no declaration of getX() present, so it turns to look at the immediate bases of ReadableData. And here things get a bit un-intuitive:

    1. It checks IReadableData, and finds IReadableData::getX(). It take note of this lookup, and the IReadableData base sub-object it was found in.
    2. It checks Data, and finds Data::getX(). A note of this lookup, and the Data base sub-object it was found in is also taken note of.
    3. Now it tries to merge the lookup sets of #1 and #2 into the lookup set of ReadableData. Since the IReadableData sub-object in #1, is also a sub-object of the Data sub-object from #2 (on account of virtual inheritance), all of #1 is completely ignored.
    4. Being left with only Data::getX() in step #3, the lookup resolves to it.


    So r.getX(); is in fact r.Data::getX();. That's what lookup finds. And it is at this point that access specifiers are checked. Which is the cause of your error.

Everything I said is an attempt to break down the process described in the standard by the section [class.member.lookup]. I didn't want to quote the standard on this, because I feel it doesn't go a lot towards explaining what happened in plain English. But you can follow the link to read the full specification.

Upvotes: 3

Nir Friedman
Nir Friedman

Reputation: 17704

Access modifiers are always resolved statically, not dynamically, i.e. it is based on the static and not dynamic type. The contract of an object implementing IReadableData, is strictly speaking that a pointer/reference to that object can be aliased as an IReadableData, and then a method getX can be called meaningfully on it. After all, the whole point of polymorphic contracts is to use them, polymorphically. There's no real guarantee, or necessity of one, on what happens when you use the derived object directly.

So, in that sense, allowing derived objects to change access specifiers, but then then resolving the access specifiers based on the static and not dynamic type, is at least a choice that is consistent with the notion of a polymorphic contract.

All that said, changing access specifiers in derived objects is not really a good idea in any way. There's no upside because it's trivial to work around, so there's zero encapsulation benefit, it just exposes this bizarre edge case.

Design wise I am not fundamentally against multiple inheritance. However, diamonds are something you are better off avoiding, 99% of the time. Private inheritance as well, has almost no uses, and they are simpler to enumerate:

  1. For the empty base class optimization.
  2. If someone else has written a class that has virtual functions as a technique to customize it, and you need this class to implement yours. I say "someone else" because this design in more modern C++ is hard to justify, now that it's easier to just pass around functions/lambda/std::function.

Upvotes: 1

Related Questions