1

I've come across a peculiar case while designing my C++ object inheritance tree in a project. I expected the below to fail:

struct A {
    protected:
    struct C { };
};

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

But, this compiles just fine. Is this like an override? I can see how this isn't ambiguous because they can be specified as A::C and B::C, but how does this act in polymorphic cases? Will it call the prescribed type's C or the instance's type's C? As in, suppose this code below works and func exists in both implementations of C:

A x = B();
decltype(x)::C::func();

which C would have their function called? More generally, is their a resource that describes how code snippets like these resolve?

Christophe
  • 68,716
  • 7
  • 72
  • 138
Anthony
  • 1,015
  • 8
  • 22
  • Your example creates `x` as having type `A` (and slices a `B` to initlalise it). In the next statement, it is irrelevant that `x` was initialised by slicing a `B`, since its type is `A`. So `decltype(x)::C::func()` will resolve to `A::C::func()` (and then compile or not depending on access control and context it is used). – Peter Mar 23 '19 at 23:09
  • @Peter. That makes sense. What if `A x = B();` was changed to`B x = B(); A &y = x;`? Would that also cause slicing? – Anthony Mar 23 '19 at 23:39
  • 1
    `B x = B()` will not cause slicing (it is equivalent to `B x` without the temporary [assuming that copying an object creates an equal object, etc]). `y` will be able to access `A::C` but not `B::C`. Given a reference to an `A`, the compiler has no information that allows it implicit to access any name in `B`, even if the referred object is actually a `B`. Given a `B` (or a reference to `B`) the name `C` in `B` hides the name `C` from `A` - it is only possible to access `A`s `C` using specific scoping (like `B::C` or `B::A::C` or (within a member of `B`) `A::C`). – Peter Mar 23 '19 at 23:58
  • @Peter That's what I was expecting. Thanks :) – Anthony Mar 24 '19 at 00:04
  • Runtime polymorphism works at... run time. Names are bound at compile time. Type names don't have runtime polymorphism. – curiousguy Apr 13 '19 at 06:02

1 Answers1

3

This is fine, because you have two unrelated struct C: the one is B::C and the other B::A::C. C is just their shortcut name.

If you would have a static function f() defined in each C, you would be able to call it from outside the struct and without any object, as you have shown in your code:

A a =  B();              // the type of a is A (B is sliced, by the way) 
decltype(a)::C::f();     // so this calls A::C::f() 

Now for the sake of completeness, here a more developed example that shows that at no point there is an ambiguity:

struct A {
   struct C { static void f() { cout << "A::C::f" <<endl; } };
};

struct B: A {
    C c1;            // the only C known here is A::C
    struct C { static void f() { cout << "B::C::f" <<endl; }}; // now B::C hides A::C
    C c2;            // so here the closest C known is B::C
    A::C c3;         // and here I can be explicit about the C I want
    using D=A::C;    // and here another way to be explicit:
    D c4; 
};

int main() {    
    B  b; 
    decltype(b.c1)::C::f();    // A
    decltype(b.c2)::C::f();    // B  
}

online demo

Christophe
  • 68,716
  • 7
  • 72
  • 138
  • So as types they won't have naming conflicts, but do you think this what result in unexpected behavior in some way? – Anthony Mar 23 '19 at 23:33
  • 1
    @AnthonyMonterrosa No, I don't think that this will give unexpected behavior. The choice of the type is always done at compile time, not at runtime. If you declare a C object, the compiler must know which C is meant (e.g. in order to generate the code that will reserve enough space for the object and construct it). – Christophe Mar 23 '19 at 23:40
  • I see. Thank you for your comprehensive example! It helps quite a bit to see the edge cases. Knowing it's always compile time also reinforces the semantics I already know. – Anthony Mar 24 '19 at 00:03