C++ Name Hiding: Why a Derived Function Hides Base Overloads and How using Fixes It

Key takeaways

Why declaring a function in a derived class hides every base-class overload with that name, how a using-declaration restores them, and how this differs from overriding.

Introduction: “Base Class Function Not Visible”

If a derived class declares a member function with the same name as one in its base, every base-class overload with that name disappears from calls made through the derived type. It does not matter that the parameter lists differ; the name alone is enough.

// ❌ Name hiding
class Base {
public:
    void foo(int x) {
        std::cout << "Base::foo(int): " << x << '\n';
    }
    
    void foo(double x) {
        std::cout << "Base::foo(double): " << x << '\n';
    }
};

class Derived : public Base {
public:
    void foo(std::string s) {
        std::cout << "Derived::foo(string): " << s << '\n';
    }
};

int main() {
    Derived d;
    d.foo("Hello");  // OK
    d.foo(42);       // ❌ Compile error: no matching function
    d.foo(3.14);     // ❌ Compile error
}

GCC reports it like this:

error: no matching function for call to 'Derived::foo(int)'
note: candidate: 'void Derived::foo(std::string)'
note:   no known conversion for argument 1 from 'int' to 'std::string' {aka 'std::__cxx11::basic_string<char>'}

The telling detail is the candidate list: it names only Derived::foo. The base overloads are not rejected, they were never considered. When a “no matching function” error lists fewer candidates than you know exist, name hiding is the first thing to suspect.

That case at least fails to compile. The sections below explain the lookup rule behind it, the more dangerous variant that compiles and calls the wrong function, how a using-declaration restores the base overloads, and how hiding interacts with virtual overriding.


What is Name Hiding?

Name Hiding Occurs

class Base {
public:
    void foo(int x) {
        std::cout << "Base::foo(int)\n";
    }
};

class Derived : public Base {
public:
    void foo(double x) {  // Hides Base::foo(int)
        std::cout << "Derived::foo(double)\n";
    }
};

int main() {
    Derived d;
    d.foo(3.14);  // Derived::foo(double)
    d.foo(42);    // ⚠️ compiles! calls Derived::foo(double), NOT Base::foo(int)
}

Reason: Derived class foo completely hides base class foo.

This is the dangerous form of name hiding, and it is worth understanding why it compiles. When the compiler sees d.foo(42), it first performs name lookup: it searches the scope of Derived, finds a member called foo, and stops. It never looks into Base. Only then does overload resolution run, and it only considers the candidates lookup found. Here that is just Derived::foo(double), and since int converts implicitly to double, the call is valid. The program compiles, runs, and quietly calls a different function than the author intended.

The example in the introduction fails loudly only because int cannot convert to std::string. Whenever the derived signature can accept the argument through a conversion (numeric types, const char* to std::string, a base class pointer, a type with a converting constructor), hiding changes behavior silently. That is much harder to find than a compile error. The usual symptom is a unit test for the derived class that exercises “the same” call as the base class test and gets a different result.

Why does C++ work this way? The rule protects derived classes from changes in their bases. If lookup merged all overloads across the hierarchy, adding a new, better-matching overload to a base class in a library update could silently redirect existing calls in derived classes. Stopping at the first scope that declares the name means a derived class’s own declarations always take precedence, and it is the same rule that makes a local variable hide a global one. The cost is the surprise shown above, which is why the fix is explicit: you opt back in with using.


using Declaration

Solution: using Declaration

class Base {
public:
    void foo(int x) {
        std::cout << "Base::foo(int)\n";
    }
    
    void foo(double x) {
        std::cout << "Base::foo(double)\n";
    }
};

class Derived : public Base {
public:
    using Base::foo;  // ✅ Bring base class foo
    
    void foo(std::string s) {
        std::cout << "Derived::foo(string)\n";
    }
};

int main() {
    Derived d;
    d.foo(42);       // Base::foo(int)
    d.foo(3.14);     // Base::foo(double)
    d.foo("Hello");  // Derived::foo(string)
}

A few properties of the using-declaration are worth knowing:

  • It brings in all overloads of that name from the base. There is no syntax to import only foo(int). If you need exactly one, write a forwarding function instead: void foo(int x) { Base::foo(x); }.
  • Access is determined by where the using-declaration is placed in the derived class. using Base::foo; in the public: section makes the imported overloads public in Derived, even if they were protected in Base. That is occasionally useful, and occasionally an accidental widening of an interface, so place it deliberately.
  • If the derived class declares an overload with exactly the same parameters as an imported one, the derived version wins for that signature. The rest of the base overloads remain visible.
  • Constructors are different. using Base::Base; is a separate feature, inheriting constructors (C++11), and it has its own rules.

The alternative to a using-declaration is qualifying the call: d.Base::foo(42) names the base scope directly and skips the hiding. That works as a one-off at a call site, but it also disables virtual dispatch (a qualified call always calls exactly Base::foo), and it pushes knowledge of the hierarchy onto every caller. As a fix for a class’s interface, using in the class is almost always the better choice; the qualified form is mainly useful inside a derived override that wants to call the base implementation.


Overloading vs Overriding

Overloading: Same Name, Different Signature

class MyClass {
public:
    void foo(int x) {}
    void foo(double x) {}      // Overloading
    void foo(std::string s) {} // Overloading
};

Overriding: Redefining Virtual Function

class Base {
public:
    virtual void foo(int x) {
        std::cout << "Base::foo\n";
    }
};

class Derived : public Base {
public:
    void foo(int x) override {  // Overriding
        std::cout << "Derived::foo\n";
    }
};

Name Hiding vs Overriding

class Base {
public:
    virtual void foo(int x) {
        std::cout << "Base::foo(int)\n";
    }
    
    virtual void foo(double x) {
        std::cout << "Base::foo(double)\n";
    }
};

class Derived : public Base {
public:
    void foo(int x) override {  // Override Base::foo(int)
        std::cout << "Derived::foo(int)\n";
    }
    // Base::foo(double) is hidden!
};

int main() {
    Derived d;
    d.foo(42);    // Derived::foo(int)
    d.foo(3.14);  // ❌ Compile error
}

Solution:

class Derived : public Base {
public:
    using Base::foo;  // ✅ Bring Base::foo(double)
    
    void foo(int x) override {
        std::cout << "Derived::foo(int)\n";
    }
};

The virtual case has an extra twist. Overriding foo(int) hides foo(double) for calls made through a Derived object or Derived*, but calls made through a Base& or Base* still see both overloads, because name lookup then happens in Base. So d.foo(3.14) fails (or converts, if a conversion exists) while static_cast<Base&>(d).foo(3.14) works. The same object behaves differently depending on the static type of the expression, which is confusing in code reviews.

Let the compiler warn you

GCC and Clang have -Woverloaded-virtual, which reports exactly this situation for virtual functions: “virtual void Base::bar(double) was hidden”. Recent Clang versions enable it as part of -Wall, and GCC 13 and newer enable a basic level of it with -Wall too. On older compilers, add it explicitly. It does not catch the non-virtual case in section 1, where the only protection is knowing the rule and, in larger codebases, a static analysis rule for hidden base members.

In my experience, name hiding bites hardest during refactoring rather than when code is first written. Someone adds a convenience overload to a derived class, say send(std::string_view) next to an inherited send(const Message&), and suddenly every call site that relied on an implicit conversion to Message either stops compiling or, worse, converts to the new overload. When I add an overload to a class that has a base with the same function name, I now add the using line in the same commit by default.


The Template Version: Members of a Dependent Base

A different rule produces a very similar “the base function is not there” error in class templates, and the fix looks similar enough that the two are often confused:

template <typename T>
struct Base {
    void log(const char* msg) { /* ... */ }
};

template <typename T>
struct Derived : Base<T> {
    void run() {
        log("start");   // error
    }
};
error: there are no arguments to 'log' that depend on a template parameter,
       so a declaration of 'log' must be available [-fpermissive]

This is not hiding, because Derived does not declare its own log. The problem is that Base<T> is a dependent base: it could be specialized for some T to have no log at all, so the compiler does not search it for unqualified names while parsing the template. MSVC in its older permissive mode accepted this code, which is why it often shows up when a codebase is first built with GCC or Clang, or with /permissive-.

There are three fixes, and all of them tell the compiler “look this up later, in the base”:

this->log("start");      // most common
Base<T>::log("start");   // works, but suppresses virtual dispatch
using Base<T>::log;      // at class scope, then plain log("start") works

I prefer this-> for one or two calls and the using-declaration when the derived template uses a base member everywhere. Base<T>::log is the one to avoid for virtual functions, for the same reason as the qualified call above.

Hiding Is About Names, Not Just Functions

The lookup rule applies to every kind of member name, which produces a few less obvious variants:

  • A data member or nested type in the derived class named foo hides all base member functions called foo as well. Calls then fail with “foo cannot be used as a function” or similar, which does not mention hiding at all.
  • A static member function hides non-static base overloads of the same name and vice versa; static is not part of the rule.
  • Private declarations in the derived class still hide. Access checking happens after lookup, so a private Derived::foo(double) hides a public Base::foo(int) and then produces an “is private within this context” error for a call that the author thought was going to the base.

The last one is the most confusing in practice, because the error message points at an access problem rather than at the hiding that caused it.


Keeping base overloads visible

Add using Base::func; whenever a derived class declares a function with the same name as base class functions and callers should still reach the base overloads. Leave it out only when hiding them is the point, and then say so in a comment, because a reader will otherwise assume the using line was forgotten.

The practical safeguard is to add the using line in the same change that introduces the derived overload, and to mark every overriding function with override so that a signature mismatch becomes a compile error instead of a new, hiding function. GCC and Clang can also warn with -Woverloaded-virtual when a derived declaration hides virtual functions of the base.