Surprises with name hiding in C++
Tags: programming
Again, a tale from the trenches, i.e. the course on C++ programming taught by my colleague Filip Sadlo. This time, it is about the surprises that occur with name hiding in C++. Take the following example of a simple class hierarchy. No virtual functions, no funny stuff going on:
class A
{
public:
void a() {}
};
class B : public A
{
public:
void a(int) {}
};
int main()
{
B b;
b.a();
}This code looks very innocent—but it does not compile. The
compiler complains that there is no matching function for the call. The
output of g++ (version 5.3.0) is rather terse:
foo.cc: In function ‘int main()’:
foo.cc:16:7: error: no matching function for call to ‘B::a()’
b.a();
^
foo.cc:10:8: note: candidate: void B::a(int)
void a(int) {}
^
foo.cc:10:8: note: candidate expects 1 argument, 0 provided
clang++ (version 3.7.0) is more helpful for beginners:
foo.cc:16:5: error: too few arguments to function call, expected 1, have 0;
did you mean 'A::a'?
b.a();
^
A::a
foo.cc:4:8: note: 'A::a' declared here
void a() {}
What is going on here? This is a classical case of name hiding. Since
class B does not contain an override for A::a(), this function is
hidden by the compiler. In § 10.2, the C++ standard meticulously
tells you that the “lookup set” that is used to, well, look
up names is filled by the derived class first. § 10.2.5
explicitly states that base classes are only ever visited if the lookup
set is empty—which is clearly not the case here.
We can fix this in multiple ways:
- We could add
using A::a;in the body ofB. Thus, we explicitly signal the compiler that we want this name to be included. - We could provide the proper scope when calling
a()by writingb.A::a();instead ofb.a(). Yes, that is horrible, but it actually works.
Of course, the real question is why the designers of C++ thought that
this behaviour is useful. From a technical point of view, visiting base
class to look up further names is a trivial matter. However, I would
firmly argue that this does not make any sense. The addition of
B::a(int) was a deliberate act made by the programmer. For me, this
signifies that the programmer wants to change the interface of the
class. If the programmer wants to keep the interface of A as well,
this should warrant additional work, such as the using declaration.
Furthermore, this behaviour makes sense because it prevents ambiguities
in the inheritance process (which I just realized sounded a lot
like something a lawyer would say!). Suppose, we had a function
A::a(float) and a function B::a(double). If A::a(float) was not
hidden by default in B, we would call the base class function when
calling b.a(0.f), even though a float can be promoted to a double.
The real fun with these ambiguities would start when a 0 is used
instead of a nullptr in C++11—since a function with an integral
parameter will always be a better match than a function taking a pointer
parameter, this would result in agonizing, hard-to-trace bugs…
So, in short: Name hiding. It’s there for a reason.