Name-lookup of nested classes with inheritance
Asked Answered
C

2

9

Is this guaranteed to work:

struct A
{
  struct Gold {};
};

struct B : public A
{
  typedef Gold BaseGold;
  struct Gold {};
};

struct C : public B
{
  typedef Gold BaseGold;
  struct Gold {};
};

static_assert(is_same<B::BaseGold, A::Gold>::value, "Not the right treasure!");
static_assert(is_same<C::BaseGold, B::Gold>::value, "Not the right treasure!");

It seems to work on VS2010. Obviously it relies on subtle declaration order/name lookup rules, so I was wondering what the standard says on the matter...

Conferee answered 12/8, 2011 at 12:30 Comment(6)
For those of us without VS2010, what is the behaviour you observe?Supercolumnar
@Oli: I think he refers to the fact that Gold has two meanings inside the derived type, first it is used in the typedef to refer to the type enclosed in the base class, and then redefined to be a local type. I assume that the behavior in VS2010 is that it allows it, get's the intended type in the typedef but the name is then reused for the type enclosed in the derived type. I am quite sure this is incorrect, but I haven't found the quote from the standard yet.Pyre
Why don't you just do typedef A::Gold BaseGold; and then the question just goes away?Vaporimeter
@Mark B: The whole point is to automatically identify a base-class aspect without explicitly mentioning it again.Conferee
@Oli: As david correctly assumes, I observe that this compiles, i.e. the static_asserts do not fire.Conferee
Compiles fine for me with gcc (using -std=c++0x - version 4.5.2)Flanker
A
8

Undefined behavior.

3.3.7/1

The following rules describe the scope of names declared in classes:

2) A name N used in a class S shall refer to the same declaration in its context and when re-evaluated in the completed scope of S. No diagnostic is required for a violation of this rule.

Accordant answered 12/8, 2011 at 14:8 Comment(14)
So to rephrase that: Whenever I add a nested class X in Derived, where Base::X already exists, I'm actually causing undefined behaviour?Conferee
Or are you just not allowed to refer to Base::X before the declaration of Derived::X (in the classes scope)?Conferee
Base::X is unambiguous before and after the declaration of Derived::X. The ambiguity results when you don't qualify the Base:: member.Vaporimeter
@ltjax: No, you can hide a name, but then it gets hidden in the entire scope where you redefined it. So you cannot use X to refer to Base::X and then redefine it to refer to some other entity, because X would have two different meaning in the same scope.Zacharyzacherie
What if you never refer to Gold (without an explicit qualifier) after it has been hidden? Only the unqualified Gold of the base class is used in every single class declaration. Doesn't that make it okay?Conferee
@Itjax, if you hide Gold, you may not refer to the hidden definition with an unqualified name.Accordant
@ltjax: That does not make it right. The definition of Gold after the typedef is what causes the problem. Basically when you add a member, the member is not added to the bits and pieces of the class that are below that line, but rather to the whole class, from opening to closing brace. That means that the typedef conceptually would refer to the Gold declared in the same class, but the language is defined to be parsed in an up-down fashion, which means that the typedef will not be reevaluated.Pyre
Consider: struct test : Base { Gold a; Gold f(); struct Gold{}; Gold b; };. There the two members of test are a and b, both declared as Gold ? in exactly the same context and yet their types would differ. In the context of the declaration of f, Gold means Base::Gold but in a later definition (outside of the class definition), test::Gold test::f() { return a; } the return type refers to a completely different type.Pyre
@David, I don't think this is the rationale. { typedef int x; { x a; typdef double x; x b; }} is well defined at block scope. I think the rationale must be something like struct test : Base { void f() { Gold x; Silver y; } struct Gold {}; Struct Silver{}; };Accordant
@AProgrammer: Yes, that's what I meant. In David's example, the unqualified Gold is mentioned again, which we avoid. In fact, the only thing referencing the "new" gold declaration in B would be class C.Conferee
@AProgrammer: Without the typedef (or the definition of a member variable) your example is well defined. The lookup for the definitions of member functions encloses the whole class, and f will use test::Gold and test::Silver as defined in test, rather than Base::Gold or anything else. Other than that, if you add a member variable or a member type before those definitions, the program becomes ill defined. That modified example is just a variant of the one I tried to provide. It wasn't the best example if it was misunderstood :)Pyre
@ltjax: It is ill formed whether you use the unqualified name again or not. Note that all external users of the class will use test::Gold, and that is a usage of the newly defined type. Or maybe I did not understand what you mean by avoiding mentioning the unqualified Gold (note that if you used test::Gold you would still be referring to the newly defined Gold, not to the inherited one, and again two entities in potentially the same scope would use the same identifier to refer to different types.Pyre
@David, I provided two examples. The first was intented to show valid code at block scope and thus explain why I don't think that preventing the same code at class scope isn't a good rationale for the rule. The second is intended to show a more confusing case without the rule: allowing the name lookup for the whole definition of test::f to be done at the end of the class definition. An alternative would have been to prevent referencing non declared yet member in inline functions but whould have the confusing fact that moving a definition inline could change its meaning.Accordant
@AProgrammer: This looks like something that can be more easily discussed in the chat. I have just logged in to the C++ Lounge, if you want we can create a different room for the discussion. Just a thought: Your second example is a good example of why it is a good rationale to disallow the same behavior of your first example.Pyre
C
1

Since there's been no quote yet, I've been playing around with your example:

Both gcc 4.5.1 and Clang 3.0 accept the code as can be seen below.

Now, we just need someone to dig out an authoritative answer. With Clang, gcc and VC++ agreeing though (not that frequent), it seems intended.

On ideone (4.5.1):

#include <utility>

struct A
{
  struct Gold {};
};

struct B : public A
{
  typedef Gold BaseGold;
  struct Gold {};
};

struct C : public B
{
  typedef Gold BaseGold;
  struct Gold {};
};

static_assert(std::is_same<B::BaseGold, A::Gold>::value, "Not the right treasure!");
static_assert(std::is_same<C::BaseGold, B::Gold>::value, "Not the right treasure!");

On Clang:

#include <stdio.h>

template <typename T, typename U>
struct is_same { enum { value = false }; };

template <typename T>
struct is_same<T,T> { enum { value = true }; };

struct A
{
  struct Gold {};
};

struct B : public A
{
  typedef Gold BaseGold;
  struct Gold {};
};

struct C : public B
{
  typedef Gold BaseGold;
  struct Gold {};
};

int main() {
  if (!is_same<B::BaseGold, A::Gold>::value) {
    printf("oups");
  }
  if (!is_same<C::BaseGold, B::Gold>::value) {
    printf("oups");
  }
}

Clang output (as expected):

define i32 @main() nounwind readnone {
entry:
  ret i32 0
}
Ciaracibber answered 12/8, 2011 at 14:3 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.