6

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?

7
  • Qualified name N::foo suppresses ADL. So the name lookup for N::foo is only performed at the point of declaration of b_req, but not at the point of instantiation. At the point of declaration, foo(S1) is not visible yet. It's not about the namespace - in your first example, you'd get the same effect if you write ::foo(obj) in b_req. It's all about the differences between qualified and unqualified name lookup. Commented May 6 at 14:51
  • @igor-tandetnik And is there a way to make behavior consistent with or without namespace? Commented May 6 at 14:54
  • Since you are trying to rely on ADL and two-phase name lookup, I suspect it would only work if S1 and foo(S1) are in the same namespace. Or else, if all overloads are visible from where b_req is defined. Commented May 6 at 15:00
  • First code would also work with namespace, IF class and function belongs to same namespace: namespace N { struct S3{}; void foo(S3) { std::puts("foo(S3)"); } }. Demo Commented May 6 at 15:38
  • @igor-tandetnik I don't get what it has to do with ADL. The calls are qualified so, as you said yourself, ADL is suppressed ? Commented May 6 at 16:18

1 Answer 1

8

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.

Sign up to request clarification or add additional context in comments.

5 Comments

Thx, so to put it bluntly, if you want to look for all possible candidates at instantiation point, you need ADL to be active, otherwise the lookup only occurs at the point of definition?
Side not, is there a strong reason, in the langage, not to defer function name lookup to point of instantiation?
@Oersted yes, because that prevents typos and errors in a template from being found prior to instantiation. Templates are not macros. If I type prontf in a template, we want that typo found before we instantiate (if possible). ADL - looking up functions based on the arguments - is the exception, because it cannot be done a the point of definition (if the types being looked up are dependent)
In practice, deferring all lookup until instantiation was a long standing "feature" of MSVC and it led to pretty pathological weirdness. It also made it nearly impossible to avoid ODR violations, which are real problems that lead to heizenbugs.
Ah I see, but then, why not keeping also a second lookup at instantiation point? It's for the ODR issue? Yet with template specialization (which is another story entirely) you may have also conflicting specializations at the instantiation point and the "risk" is accepted?

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.