Surprises with name hiding in C++

Tags: programming

Published on
« Previous post: Improved superscript citations for … — Next post: A git hook to keep your emotions in … »

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:

  1. We could add using A::a; in the body of B. Thus, we explicitly signal the compiler that we want this name to be included.
  2. We could provide the proper scope when calling a() by writing b.A::a(); instead of b.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.