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.