2

I accidentally run into the problem having member variables with the same name in classes used in multiple inheritance. My basic idea was that the member variables are simple "merged", i.e. a multiple declaration happens. The compiler did not tell me even a warning, see the MWE below. I understand that it is a bad idea to have variables with the same name, so I think it is at least ambiguous to refer to them in the way I do; so I expected at least a warning or maybe an error.

1) Why the compiler does not write out at least a warning?

2) How handling of those variables is solved internally? (I guess aliases like HW::I and Other::I are used, but how they relate to SW1::I and SW2::I?)

#include <iostream>
struct Other { int I;};
struct HW { int I;};
struct SW1 : Other, HW { int I;};
struct SW2 : HW, Other { int I;};
struct D : SW1 { };
struct E : SW2 { };

int main()
{
    E* e = new E;
    D* d = new D;
    e->I = 3;
    SW1* pc1 = dynamic_cast<SW1*>(d);
    pc1->I = 2;
    std::cerr << d->I;
    std::cerr << e->I;
    SW2* pc2 = dynamic_cast<SW2*>(e);
    pc2->I = 1;
    std::cerr << d->I;
    std::cerr << e->I;
}
Cody Gray - on strike
  • 239,200
  • 50
  • 490
  • 574
katang
  • 2,474
  • 5
  • 24
  • 48

3 Answers3

4

Why the compiler does not write out at least a warning?

Because you didn't write anything wrong, dangerous or ambiguous. You or I may be confused, but the compiler has a specific set of lookup rules to handle it.

When you write a class member access expression like e->I, the compiler doesn't just look up the name I, it looks up the sub-object that contains the member named this way, along with the member. It also starts with the most derived object type, and looks "up" at base class sub-object until it finds something (this is also how member name hiding in C++ works, in a nutshell).

So for e->I, it looks for I in E. That search finds nothing, so it goes into the base class subject. It find SW2::I, a name that refers to a unique member defined in SW2. So it stops.

If there was no SW2::I, it will continue looking and find both Other::I and HW::I. Now the same name is found in two different base class sub-object, and we get an ambiguity. Not a warning of ambiguity, but flat out making the expression e->I ambiguous, which is an error.

Johannes Overmann
  • 4,914
  • 22
  • 38
StoryTeller - Unslander Monica
  • 165,132
  • 21
  • 377
  • 458
  • As you wrote "you and I may be confused". I am separating a class along separable functionality to two other classes, the original is derived with multiple inheritance. I, by mistake, accidentally cloned a member in both new classes, resulting in the need of a half-day debugging. Although I understand that nothing wrong, and it can be useful in some cases, in my particular case, a warning that "it is a bad practice" would save some time. – katang Feb 04 '19 at 10:55
  • @katang - C++ implementations are not so big on hand holding. Ultimately, unless something is understood by the greater C++ community as a bad practice, the implementation is going to assume you know what you are doing. Hiding member names is not understood as a bad practice on the grander scale (after all, you do that every time you override a function too). – StoryTeller - Unslander Monica Feb 04 '19 at 10:57
2

The compiler is correct not to diagnose any problems with your code. The code, as you've constructed it is not ambiguous. Essentially, a name is ambiguous if it is an equally good match for more than one variable (or class member in your case).

When evaluating e->I, the first candidate found is the I that is a member (via inheritance) of class SW2. The members of I that SW2 inherits from its base classes are not as good a match as the member defined directly by Sw2.

Similarly, pc1->I is unambiguously the member of SW1, d->I is the same, and pc2->I is unambiguously the member of the base class SW2.

Ambiguity would occur in evaluating e->I if SW2 did not have its own member named I (i.e. struct SW2: HW, Other {};( . In that case, when evaluating e->I, the name resolution looks in SW2 for a member named I, and doesn't find it. Resolving the name then considers the two base classes, HW and Other, which both have a member named I. They are equally good matches, so the expression e->I is ambiguous - and the compiler will issue a diagnostic i.e. an error (not just a warning). In that case, it is possible for the programmer to explicitly resolve the ambiguity using the scope (::) operator. For example, e->HW::I or e->Other::I which fully qualifies the name.

You are right that such multiple usage of a name within a class hierarchy is a bad idea. Both because it can be difficult to correctly resolve the ambiguity in a way that makes sense to a compiler, and because mere mortals will often have trouble following the logic.

Peter
  • 35,646
  • 4
  • 32
  • 74
  • " because mere mortals will often have trouble following the logic" including me, who did this without any intention to do so. – katang Feb 04 '19 at 10:54
  • @katang - them's the breaks. It can be confusing, but the behaviour is not particularly dangerous (e.g. no undefined behaviour, poisoning unrelated memory, etc) and is useful in some scenarios. When using constructs that are useful but not particularly dangerous if the programmer gets confused the philosophy in C++ is that the programmer needs to understand their design, not be hand-held by a compiler. Giving warnings in cases like yours means also annoying developers who have deliberately exploited the language feature. – Peter Feb 04 '19 at 11:25
1

The variables are not merged, you simply get all 3 of them at the same time. You need to cast the pointer to the right type to access the variable you want.

#include <iostream>
struct Other { int I; };
struct HW { int I; };
struct SW1 : public Other, public HW { int I; };
struct D : public SW1 { };

int main() {
    D* d = new D;
    d->I = 1;
    SW1* pc1 = dynamic_cast<SW1*>(d);
    pc1->I = 2;
    static_cast<Other*>(pc1)->I = 3;
    static_cast<HW*>(pc1)->I = 4;
    std::cerr << d->I;
    std::cerr << static_cast<Other*>(d)->I;
    std::cerr << static_cast<HW*>(d)->I;
}

Prints:

234

I.e. the same object d contains 3 different versions of I, depending on how you view it.

rustyx
  • 80,671
  • 25
  • 200
  • 267