C++ private virtual inheritance problem
Asked Answered
G

3

19

In the following code, it seems class C does not have access to A's constructor, which is required because of the virtual inheritance. Yet, the code still compiles and runs. Why does it work?

class A {};
class B: private virtual A {};
class C: public B {};

int main() {
    C c;
    return 0;
}

Moreover, if I remove the default constructor from A, e.g.

class A {
public:
    A(int) {}
};
class B: private virtual A {
public:
    B() : A(3) {}
};

then

class C: public B {};

would (unexpectedly) compile, but

class C: public B {
public:
    C() {}
};

would not compile, as expected.

Code compiled with "g++ (GCC) 3.4.4 (cygming special, gdc 0.12, using dmd 0.125)", but it has been verified to behave the same with other compilers as well.

Gillian answered 3/3, 2010 at 12:29 Comment(2)
With g++ 4.4 it compiles. While I have not been able to find an authoritative reference, my believe is that it should compile. The most derived class C can construct the subobject of type A. Note that there are implementations to seal inheritance based on the combination of private virtual inheritance together with a private constructor in A and access granted to B through friendship. All the complication would be unnecessary if just using private virtual inheritance would suffice.Maddock
@DavidRodríguez-dribeas "All the complication would be unnecessary if just using private virtual inheritance would suffice." Nobody here claimed that the sealing idiom works without a private ctor. In the sealing idiom, the private inheritance is not needed, but it is needed to make the use of the idiom an implementation detail.Colettecoleus
F
14

According to C++ Core Issue #7 class with a virtual private base can't be derived from. This is a bug in compiler.

Fairleigh answered 3/3, 2010 at 12:52 Comment(6)
Excepted that g++ and como aren't complaining about the example in the core issue. That's enough to make me doubt of my answer but I'd like a more recent reference than one of the older issues which could very well not have been updated if the rules changed.Merodach
As a matter of fact, this issue has been closed, meaning it is not an issue.Divulge
"class with a virtual private base can't be derived from" Wrong.Colettecoleus
@curiousguy, check the link in the answer. This is official "bug tracker" of the C++ committee.Fairleigh
This bug tracker contains some nonsense that will never be corrected because nobody cares. It is even less relevant than the C++ standard on this matter. There is universal consensus on what virtual inheritance means.Colettecoleus
+1 for referencing a pertinent document. -1 because the document is wrong.Indignant
P
6

For the second question, it is probably because you don't cause it to be implicitly defined. If the constructor is merely implicitly declared, there is no error. Example:

struct A { A(int); };
struct B : A { };
// goes fine up to here

// not anymore: default constructor now is implicitly defined 
// (because it's used)
B b;

For your first question - it depends on what name the compiler uses. I have no idea what the standard specifies, but this code for instance is correct because the outer class name (instead of the inherited class name) is accessible:

class A {};
class B: private virtual A {};
class C: public B { C(): ::A() { } }; // don't use B::A

Maybe the Standard is underspecified at this point. We'll have to look.


There does not seem to be any problem with the code. Furthermore, there is indication that the code is valid. The (virtual) base class subobject is default initialized - there is no text that implies that name lookup for the class name is dine inside the scope of C. Here is what the Standard says:

12.6.2/8 (C++0x)

If a given non-static data member or base class is not named by a mem-initializer-id (including the case where there is no mem-initializer-list because the constructor has no ctor-initializer) and the entity is not a virtual base class of an abstract class

[...] otherwise, the entity is default-initialized

And C++03 has similar text (thou less clear text - it simply says its default constructor is called at one place, and at another it makes it dependent on whether the class is a POD). For the compiler to default initialize the subobject, it just has to call its default constructor - there is no need to lookup the name of the base class first (it already knows what base is considered).

Consider this code that certainly is intended to be valid, but that would fail if this would be done (see 12.6.2/4 in C++0x)

struct A { };
struct B : virtual A { };
struct C : B, A { };
C c;

If the compiler's default constructor would simply look-up class name A inside of C, it would have an ambiguous lookup result with regard to what subobject to initialize, because both the non-virtual A and the virtual A's class-names are found. If your code is intended to be ill-formed, i would say the Standard certainly needs to be clarified.


For the constructor, notice what 12.4/6 says about the destructor of C:

All destructors are called as if they were referenced with a qualified name, that is, ignoring any possible virtual overriding destructors in more derived classes.

This can be interpreted in two ways:

  • calling A::~A()
  • calling ::A::~A()

It seems to me that the Standard is less clear here. The second way would make it valid (by 3.4.3/6, C++0x, because both class-names A are looked up in global scope), while the first will make it invalid (because both A will find the inherited class names). It also depends what subobject the search starts with (and i believe we will have to use the virtual base class' subobject as start point). If this goes like

virtual_base -> A::~A();

Then we will directly find the virtual base' class name as a public name, because we won't have to go through the derived class' scopes and find the name as non-accessible. Again, the reasoning is similar. Consider:

struct A { };
struct B : A { };
struct C : B, A {
} c;

If the destructor would simply call this->A::~A(), this call would not be valid because of the ambiguous lookup result of A as an inherited class name (you cannot refer to any non-static member-function of the direct base class object from the scope C, see 10.1/3, C++03). It will uniquely have to identify the class names that are involved, and has to start with the class' subobject reference like a_subobject->::A::~A();.

Pain answered 3/3, 2010 at 13:0 Comment(14)
"it depends on what name the compiler uses." ??? No name is "used", and there is no issue of name lookup. Constructors are called. They'd better be accessible.Colettecoleus
@curious your comment does not make sense to me. Are you asking what '"it depends on what name the compiler uses."' means? Why are you making "Constructors are called." bold? FWIW, access checking is done on names, not on anything else. It also is done on con/destructors but that's something not well described in the Standard. You didn't read my complete answer it seems. It quotes "All destructors are called as if they were referenced with a qualified name" - "no issue of name lookup"? Funny. You need to be clearer about your concerns...Pain
"FWIW, access checking is done on names," No. Access checking is done on declaration used. The declaration is not accessible here. "It also is done on con/destructors but that's something not well described in the Standard." Many things go without saying. "It quotes "All destructors are called as if they were referenced with a qualified name" - "no issue of name lookup"?" Which issue of name lookup?Colettecoleus
@curious since this answer has been downvoted again by apparently someone who just read your comment, I would like to give you 11p4 of C++11 for the first part of your last comment: "Access control is applied uniformly to all names, whether the names are referred to from declarations or expressions.". FWIW, the introduction of a name is always a declaration. So "the declaration used" is equivalent to "the name used" (the meaning of "declaration" is two fold. one refers to the syntactic construct, and the others refers to introduction of names).Pain
"Many things go without saying." I've been working with the spec for years, and if I don't get how this works, then it does not go without saying (and if you disagree, then bring up on what you disagree and why, and not just plain statements into the air like "No issue of name lookup"/"Many things go without saying". I'm sorry, your comments look like an act of trolling of someone who thinks he is smarter than others).Pain
"your comments look like an act of trolling" I was going to try to explain to you, but maybe you are not really interested.Colettecoleus
"I've been working with the spec for years" then forget the spec. Without looking at the spec, intuitively, how do you think access control is done on constructors and destructors? I think it is quite clear, but maybe it's because I don't care about the spec.Colettecoleus
@curiousguy: Since the C++ language is defined by its specs, one should never "forget the spec" and never rely on "intuition" for an implementation detail. The fact is that the spec is fact.Corned
@ThomasEding You know, I have been in the C++ committee, so I know what the C++ spec is, and how much it is useful. It is lacking in many, many ways. It has many flaws and ambiguities. The spec is not "fact" (what should that mean?), it is a attempt at describing the C++ language. Whenever there is something clearly wrong in the C++ spec, competent people ignore it - every time. You do not appear to know the C++ spec well, not to know how the C++ standardization works, so please do not make definitive statements about things you do not understand at all.Colettecoleus
@curiousguy: The spec is fact in that it is how the language is defined. There is no dodging that. If you want to write a compiler, it has to follow the spec if you are implementing that specific language. End of story. I'm not saying whether or not the language is flawed from a design perspective. That is a totally different issue. My main point in my previous comment is to not intuitively guess how a language ought to work. The language's specification is the definative source for figuring out what something will do in a language (including whether it is UB and other stuff).Corned
@curiousguy: it is a attempt at describing the C++ language... no... it is not merely an attempt; it does descsribe (a dialect of) the C++ language. If the language is found to be flawed, then people write up a new spec to handle these problems and come up with a completely new dialect (read: a new language).Corned
@ThomasEding "The spec is fact in that it is how the language is defined" No. The ISO C++ standard tries to defines the language, and then the DRs try to fix the definition. "End of story." Wrong, 100 % wrong. "I'm not saying whether or not the language is flawed from a design perspective." Did I say anything about language design? No, I did not. "My main point in my previous comment is to not intuitively guess how a language ought to work." Yes, it is. "The language's specification is the definative source" No, it is not. I know what I am taking about, and you do not.Colettecoleus
@ThomasEding "If the language is found to be flawed" No, if the spec is flawed, and if the committee cares (something the specification flaws are so serious that the committee does not even think about fixing them). The spec is only a vague, lacking, description of the C++ language. The spec, if you manage to ignore the worst flaws, defines an imaginary language that is useless of any real purpose because it is so vaguely described. "come up with a completely new dialect (read: a new language)" No. Again, you have absolutely no idea what you are taking about.Colettecoleus
Please carry out your discussion in the chat. Thanks!Pain
M
2

Virtual base classes are always initialized from the most derived classes (C here). The compiler has to check that the constructor is accessible (i.e. I get an error with g++ 3.4 for

class A { public: A(int) {} };
class B: private virtual A {public: B() : A(0) {} };
class C: public B {};

int main() {
    C c;
    return 0;
}

while your description implies there is none) but the fact that as a base, A is private or not doesn't matter (to subvert would be easy: class C: public B, private virtual A).

The reason for which the virtual base classes constructors are called from the most derived class is that it needs to be constructed before any classes having them as a base class.

Edit: Kirill mentioned an old core issue which is at odd with my reading and the behavior of recent compilers. I'll try to get standard references in one way or the other, but that can takes time.

Merodach answered 3/3, 2010 at 12:52 Comment(1)
You should say that you are only addressing the 2nd question and not the 1st.Indignant

© 2022 - 2024 — McMap. All rights reserved.