Tuesday, October 25, 2011

Behind the Scenes: Name Hiding in C++

Name hiding happens when an overloaded member function is declared in the scope of a derived class. Here is an example:
class Base
{
public:
    virtual ~Base() {}

    virtual void someFunction() const;
    virtual void someFunction(int x) const;
};

void Base::someFunction() const {}
void Base::someFunction(int x) const {}

class Derived : public Base
{
public:
    void someFunction() const;
};

void Derived::someFunction() const {}

int main(int argc, char **argv)
{
    Derived d;
    d.someFunction(123);         

    return 0;
}
This, in fact, will not compile and gcc reports the following error:
error: no matching function for call to 'Derived::someFunction(int)'
While if we remove Derived::someFunction(), the code compiles without errors. First of all, note that there are two orthogonal language features involved here: function overloading and inheritance (overloading vs. overriding).


The scope of Derived becomes nested within the scope of its base class, however, introducing a name in the derived class obscures all overloaded versions of the same function in the Base class, hence the compiler error. This is actually standard behavior, for reasons which will be discussed below.

Overload resolution

Function overloading takes place when two, or more functions have
  • the same name, and
  • the same return type, but
  • different signatures.

But what exactly constitutes a signature? My copy of the C++ Standard draft document has the following to say about function signatures:
1.3.11 signature
the information about a function that participates in overload resolution (13.3): its parameter-type-list (8.3.5) and, if the function is a class member, the cv-qualifiers (if any) on the function itself and the class in which the member function is declared.2) The signature of a function template specialization includes the types of its template arguments (14.5.5.1).
Additionally, footnote 2 says:
Function signatures do not include return type, because that does not participate in overload resolution.
In other words, a function's signature is formed by
  • the number of parameters;
  • the data type of the parameters;
  • the order in which they appear; and
  • the function's cv-qualifiers, if any (in the case of class members)

The process of determining the actual call is referred to as overload resolution, and takes place at compile time. Here are three functions with the same name and return type, but different signatures:
void scoobydoo();
void scoobydoo(int x);
void scoobydoo(int x, int y);
Overload resolution works by choosing the best function from a number of candidates, matching the arguments to the parameters of the candidates.
int main()
{
    scoobydoo(2);      // matches void scoobydoo(int x);
    return 0;
}
When identifying possible candidates, name lookup propagates outward starting from the immediate scope. Only if no function exists with the correct name does the process continue to the next enclosing scope. Once a scope is found that has at least one viable candidate, the compiler proceeds with matching the arguments to the parameters of the various candidates, applying access rules etc.
class Derived : public Base
{
public:
    void someFunction() const;
};
Since someFunction is found in the scope of Derived, the compiler doesn't look any further. Instead, it continues to select the most appropriate function from the set of candidates, which in this case only consists of the version of someFunction with no parameters.

The fragile base class problem

Now, the actual reason behind this whole name hiding business is to address something called the fragile base class problem, which refers to a situation when changes made to a base class can potentially harm code that uses derived classes. Here is an example:
class A {};

class B : public A
{
public:
    void f(float x) { std::cout << "B::f"; }
};

int main()
{
    B b;
    b.f(5);
    return 0;
}
Suppose class A is part of a library, and B is client code. Make the following change in A,
class A
{
public:
    virtual void f(int x) { std::cout << "A::f"; }
};
and all of a sudden, the int is a better match to the argument supplied in main(). Without name hiding we would have changed the behavior of the program. Instead, because of this feature, A::f() is never considered by the name lookup mechanism – it is hidden.

Solutions/work-arounds

How can we make someFunction(int x) available also in the scope of our Derived class then? Essentially, there are three different ways of doing this.

1. Use fully qualified name
A qualified call bypasses the virtual dispatch mechanism:
int main(int argc, char **argv)
{
    Derived d;
    d.Base::someFunction(123);         

    return 0;
}
This, however, is usually not a very good design choice. Was Derived ever to change, and implement someFunction(int x), client code would need to change as well.

2. Reimplementing
Another solution is to reimplement someFunction(int x) in Derived:
class Derived : public Base
{
public:
    void someFunction() const;
    void someFunction(int x) const;
};

void Derived::someFunction() const {}
void Derived::someFunction(int x) const { Base::someFunction(x); }

3. The using directive
A using declaration also allows us to re-introduce someFunction in the scope of the derived class. This, in fact, is the recommended approach.
class Derived : public Base
{
public:
    using Base::someFunction;

    void someFunction() const;
};

void Derived::someFunction() const {}

int main(int argc, char **argv)
{
    Derived d;
    d.someFunction(123);         

    return 0;
}
 

2 comments:

Joe said...

Very well written post - thanks!

Another page on name hiding in C++ that helped me:

http://www.programmerinterview.com/index.php/c-cplusplus/c-name-hiding/

Unknown said...

• Nice and good article. It is very useful for me to learn and understand easily. Thanks for sharing your valuable information and time. Please keep updatingAzure Online Training

Post a Comment