c++templatesnamespaceslanguage-lawyeroverload-resolution

function selection finding unexpected candidate when using namespaces


This is a follow-up of this question: `requires` expressions and function namespaces

Trying to design an answer I stumbled on this strange behavior:

#include <cstdio>

// dummy default declaration
template <typename T>
void foo(T) = delete;

template <typename T>
void bar(T t) {
    foo(t);
};

// client types

struct S1 {};

// "specializations" for S1
void foo(S1) { std::puts("foo(S1)"); }

int main() {
    foo(S1{});  // calls the expected version
    bar(S1{});     // try to call the deleted version the wrong version
}

ouput:

foo(S1)
foo(S1)

But if foo is inside a namespace:

// dummy default declaration
namespace N {
template <typename T>
void foo(T) = delete;
}
template <typename T>
void bar(T t) {
    N::foo(t);
};

// client types

struct S1 {};

// "specializations" for S1
namespace N {
void foo(S1) { std::puts("foo(S1)"); }
}  // namespace N

int main() {
    N::foo(S1{});  // calls the expected version
    bar(S1{});     // try to call the deleted version the wrong version
}

The call to bar does not compile as only the first occurrences of foo (the deleted templates here) are considered. The candidate void N::foo(S1) is not considered.

Yet If, instead of function definitions, I play with template specialization, the behavior is consistent:

// a kind of trait, false by default
template<typename T> struct Trait : public std::false_type
{};

template <typename T>
constexpr bool b_struct = Trait<T>::value;

// client types

struct S1 {};
struct S2 {};

// specializations for S1
template<> struct Trait<S1> : public std::true_type
{};

int main() {
    std::cout<<std::boolalpha<<b_struct<S1><<" expecting true\n";
    std::cout<<std::boolalpha<<b_struct<S2><<" expecting false\n";
}

and

// a kind of trait, false by default
namespace N {
template <typename T>
struct Trait : public std::false_type {};
}  // namespace N

template <typename T>
constexpr bool b_struct = N::Trait<T>::value;

// client types

struct S1 {};
struct S2 {};

// specializations for S1
namespace N {
template <>
struct Trait<S1> : public std::true_type {};
}  // namespace N

int main() {
    std::cout << std::boolalpha << b_struct<S1> << " expecting true\n";
    std::cout << std::boolalpha << b_struct<S2> << " expecting false\n";
}

both output:

true expecting true
false expecting false

LIVE no namespace

LIVE namespace

I try to understand through https://en.cppreference.com/w/cpp/language/qualified_lookup and https://en.cppreference.com/w/cpp/language/overload_resolution but to no avail.

Is this behavior normal and why?


Solution

  • Functions can be overloaded. Function templates can be specialized.

    These are utterly different things.

    Function template specializations do not change which function is found during overload resolution. They only change which one is executed.

    Overloads are found during overload resolution. One overload is picked.

    // "specializations" for S1
    void foo(S1) { std::puts("foo(S1)"); }
    

    this is not a specialization; it is an overload.

    I generally advise people to avoid using function template specializations, as function overloading can provide the same functionality but not the opposite. Understanding 2 different systems for no practical additional functionality is something I avoid.


    In your first case,

    template <typename T>
    void bar(T t) {
      foo(t);
    };
    

    we have an unqualified call to foo within the root namespace. This call is dependent on a template argument T.

    The compiler does two passes of overload resolution. First, it does a non-argument-dependent overload resolution at the point of definition of the template function bar. This finds the foo() = delete overload.

    Second, at the point of instantiation of bar<T>, it does an argument dependent lookup. This starts in the namespace of the type T (and related namespaces), and looks for foo overloads that take a T. In this case, the namespace of T is the root namespace, where it finds a foo overload (actually 2).

    The two overloads are ordered and one is found to be the chosen overload (the standardese text is annoyingly complex here), and that is the one that is called.


    In your second case:

    template <typename T>
    void bar(T t) {
      N::foo(t);
    };
    

    you did a fully qualified call to N::foo. No argument dependent lookup is done.

    So the only foo found is the =delete one visible at the point of definition of bar.

    Had you done this instead:

    template <typename T>
    void bar(T t) {
      using namespace N;
      foo(t);
    };
    

    it would no longer block argument dependent lookup of foo. However, as the overloads of foo(S1) are in namespace N, and struct S1 aka T is in the root namespace, searching for argument-dependent overloads of foo won't find your foo in question. This will still only find the =deleted one.

    When you invoke N::foo after the new foos are introduced, you indeed find the ones that are introduced, and the "expected" (by you) foo is called.


    The traits case is unrelated, because it is no longer about overload resolution, but instead about template classes and template class specialization. The rules for this are not very related to the rules for overload resolution; that it works differently shouldn't be a surprise.


    To make the overload version work properly, it should look roughly like this:

    namespace FuncNS {
      template <typename T>
      void foo(T) = delete;
    }
    
    template <typename T>
    void bar(T t) {
      using FuncNS::foo; // enable ADL
      foo(t);
    }
    
     // client types
    
     namespace ClientNS {
       struct S1 {};
       struct S2 {};
     }
    
     // foo overloads go in the ClientNS!
     namespace ClientNS {
       void foo(S1) { std::puts("foo(S1)"); }
     }
    
     int main() {
       using FuncNS::foo;
       foo(ClientNS::S1{});
       bar(ClientNS::S1{});
     }
    

    live example.

    Custom overloads go in the namespace of the type you want to customize for.

    If someone directly calls FuncNS::foo, they are asking to ignore any customization points.