0

I have a quite niche problem concerning private inheritance. I also have a solution for the problem, but I don't understand why it works.

TL;DR

Why does private inheritance on some intermediate level prevent me from passing the base type as a parameter to a (privately) derived class?

Consider the following code (also available here http://cpp.sh/3p5zv5): I have a composite-type class that can contain pointers to child elements of its own type. Also, that class contains the template method MyMethodTemplate(T value), allowing any type of Parameter. I need to inherit from this class multiple times, such that MyMethodTemplate(T) is not available, and instead only a typed version MyMethod() can be called with e.g. int, string, whatever.

Since the derived classes were going to contain a lot of boilerplate code (not shown here), I wrote the class template cSpecializedComposite that inherits privately from cComposite (successfully hiding MyMethodTemplate()). Its method MyMethod() internally calls MyMethodTemplate() from its parent class. So far, so good.

Now, to get rid of the template parameter in my end user code, I wanted to write trivial classes that publicly inherit from the template (cSpecializedCompositeInt, cSpecializedCompositeString, ...). My assumption was that cSpecializedCompositeInt would know cSpecializedComposite's interface, but not its internals. In cSpecializedCompositeInt's constructor, I can optionally pass a vector of unique_ptr that is passed on to its parent constructor (who does god knows what with it, nothing to see here, move along). Note that the class definition for cComposite is visible to cSpecializedCompositeInt, even if cSpecializedCompositeInt doesn't inherit from it, as far as it knows.

However, I get a compiler error C2247 in cSpecializedCompositeInt's constructor, telling me I can't use cComposite, because cSpecializedComposite inherited privately from it. This occured on both msvc10 and GCC 4.9.2 (the compiler behind http://cpp.sh).

Changing the private inheritance to protected allows cSpecializedCompositeInt to know it inherited indirectly from cComposite, and the compiler error goes away.

In how far is this related to Private Inheritance and Derived Object to Base reference ?

#include <vector>
#include <memory>

class cComposite
{
public:
  cComposite(std::vector<std::unique_ptr<cComposite>>&& vecC)
    : m_vecC(std::move(vecC))
  {
    //empty
  }

  template <typename T>
  void MyTemplateMethod(T value)
  {
    //do something with any type of value
  }

private:
  std::vector<std::unique_ptr<cComposite>> m_vecC;
};

template <typename MySpecialType>
class cSpecializedComposite : private cComposite
{
public:
  cSpecializedComposite(std::vector<std::unique_ptr<cComposite>>&& vecC)
    : cComposite(std::move(vecC))
  {
    //empty
  }

  void MyMethod(MySpecialType value)
  {
    //allow only MySpecialType as Input, call base class template method to do something
    cComposite::MyTemplateMethod(value);
  }
};

class cSpecializedCompositeInt : public cSpecializedComposite<int>
{
public:
  cSpecializedCompositeInt(std::vector<std::unique_ptr<cComposite>>&& vecC)
    : cSpecializedComposite(std::move(vecC))
  {
    //empty
  }
};

int main(int argc, char* argv[])
{
  std::vector<std::unique_ptr<cComposite>> vecC;
  cSpecializedCompositeInt spec(std::move(vecC));

  spec.MyMethod(5);

  return 0;
}
Daniel
  • 1,407
  • 1
  • 12
  • 22
  • Because private inheritance means private. Everything inherited is private. Even the base classes of the privately-inherited class, whether they're public or not. – Sam Varshavchik Feb 26 '19 at 13:40
  • I figured as much. My question is, why is the base class effectively made invisible to derived classes, as opposed to only preventing them from knowing who they inherited from, aka making that internal interface unavailable to them? – Daniel Feb 26 '19 at 13:43
  • Because `private` makes it possible for the class to change who it privately inherits from, at any time, with 0% risk of breaking any API. Since nothing except the class knows what it privately inherits from, the class's private inheritance can be changed at any time. It can be completely removed, even. Only the class itself needs to be changed because of that. Instead of publicly inheriting from the base class, consider using virtual public inheritance, instead. Now, the virtual public class is accessible from any subclass. – Sam Varshavchik Feb 26 '19 at 13:50
  • Thanks, that rationale makes sense to me. However, I can still construct and use a cSpecializedComposite in my end user code, passing cComposite in its constructor despite the template class inheriting privately from cComposite. It seems a bit strange to make the private base class completely inaccessible to derived classes, but no one else (i.e. end users). I'm sure there's a rationale behind it, and I can let it rest, but it still appears weird. – Daniel Feb 26 '19 at 14:06

1 Answers1

1

One of the recurring themes on this site is requesting a Minimal, Complete, and Verifiable example. You did do a good job with the "complete" and "verifiable" parts, but not so much with the "minimal". Please allow me to simplify your code to remove potentially distracting details.

// Base class, constructed from a type that involves itself.
class A {
public:
    A(A *) {}
};

// Intermediate class, derives privately from the base class.
class B : private A {
public:
    B(A * a) : A(a) {}
};

// Most derived class, same constructor parameter as the base class.
class C : public B {
public:
    C(A * a) : B(a) {}  /// error: 'class A A::A' is inaccessible
};

int main(int argc, char* argv[])
{
  return 0;
}

Note the lack of templates and vectors; those are just red herrings. Also, apologies for using raw pointers; they are just a convenient way to introduce the issue with minimal overhead/baggage. (I'd use references, but that would turn one of the constructors into a copy constructor, which feels unwise.)

Look at the definition of B's constructor. In that definition, "A" is used twice: once as part of the type of the parameter, and once in the initializer list. For the latter case, the use of A definitely refers to the (private) base class of the class being constructed. Question: why should the compiler assume that the former case does not also refer to the private base class? (If the private base class was changed, would the parameter's type necessarily change as well? The compiler assumes "yes", but you could introduce another intermediate class, between A and B, which presumably would preserve the parameter's type.)

As far as I know (I have not double-checked the language specs), when you are in the context of B, any mention of its private base class is considered private information. You could think of the constructor's declaration as: B(<private> * a). Since C is not allowed to know the private information of B, it is not allowed to call this constructor. It simply cannot match the parameter list, much the same as if the parameter's type was defined within a private section of B. Fortunately, this can be circumvented by removing a mention of "A" from the parameter list.

In the following, the only change is the introduction and use of a typedef.

class A;
typedef A * special_type;

// Base class, constructed from a type that *indirectly* involves itself.
class A {
public:
    A(special_type) {}
};

// Intermediate class, derives privately from the base class.
class B : private A {
public:
    B(special_type a) : A(a) {}
};

// Most derived class, same constructor parameter as the base class.
class C : public B {
public:
    C(special_type a) : B(a) {}  /// no error!
};

int main(int argc, char* argv[])
{
  return 0;
}

In your case, this would have the side benefit of introducing a shorter synonym for that rather symbol-rich std::vector<std::unique_ptr<cComposite>>.

JaMiT
  • 14,422
  • 4
  • 15
  • 31
  • Thanks for your answer. I find it mind-boggling that you can circumvent the problem by introducing a typedef, just as much as I still find it mind-boggling that C can't use a publicly available type for its B-parent-class initialization, but anyone else constructing a B instance can. – Daniel Feb 27 '19 at 06:27
  • 1
    @Daniel This is a syntax issue, not a semantic one. C can use a publicly available type to initialize B. The problem is that B cannot express that the required parameter is a publicly available type without using the typedef. – JaMiT Feb 27 '19 at 10:12