Confusion around function call resolution
B

2

7

This question is inspired by this one. Consider the code:

namespace ns {
  template <typename T>
  void swap(T& a, T& b) {
    using namespace std;
    swap(a, b);
  }
}

After some test with GCC, I found that swap(a, b); resolves to
1) std::swap if T has overloaded std::swap (e.g., standard container types)
2) ns::swap otherwise, leading to infinite recursion.
So, it seems that the compiler will first try to find a match in the namespace ns. If a match is found, the search ends. But this is not the case when ADL comes in, in which case, std::swap is found anyway. The resolution process seems to be complicated.

I want to know the details of what is going on under the hood in the process of resolving the function call swap(a, b) in the above context. Reference to the standard would be appreciated.

Belmonte answered 4/5, 2015 at 14:12 Comment(5)
Note that using-directives such as using namespace std; behave a very peculiar behaviour; they're not using-declarations such as using std::swap.Heterodox
@Heterodox Just had a deep feeling about that :-)Belmonte
As far as I can tell, this using-directive won't do anything. It makes names of namespace std visible as if they're declared in the global namespace here, but the name swap in the global namespace is hidden for pure unqualified lookup inside ns::swap because the namespace ns already contains a member of the name swap.Heterodox
So, if I understand correctly, if you change it to using std::swap, we'll get compilation error since compiler will see both std::swap and ns::swap.Multivocal
@AntonFrolov There are two mechanisms for name lookup involved here: pure unqualified lookup and argument-dependent lookup. Pure unqualified lookup will stop in the next enclosing scope where it finds the name. With using std::swap;, we tell it to stop at block scope - ns::swap will not be found, it is a member of the enclosing namespace scope. Argument-dependent lookup on the other hand searches all scopes associated with the types of the arguments.Heterodox
H
6

The code in the OP is equivalent to this:

using std::swap; // only for name lookup inside ns::swap

namespace ns {
  template <typename T>
  void swap(T& a, T& b) {
    swap(a, b);
  }
}

Why? Because using-directives like using namespace std; have a very peculiar behaviour C++14 [namespace.udir]p2:

A using-directive specifies that the names in the nominated namespace can be used in the scope in which the using-directive appears after the using-directive. During unqualified name lookup, the names appear as if they were declared in the nearest enclosing namespace which contains both the using-directive and the nominated namespace.

The nearest enclosing namespace that contains both namespace std and the block scope of function ns::swap is the global namespace.

Using-declarations such as using std::swap; on the other hand really introduce names into the scope in which they appear, not in some enclosing scope.


The lookup of a function call expression such as swap(a, b) is called unqualified lookup. The identifier swap has not been qualified with any namespace or class name, as opposed to ns::swap, which has been qualified via ns::. Unqualified lookup for potential names of functions consists of two parts: pure unqualified lookup and argument-dependent lookup.

Pure unqualified lookup stops at the nearest enclosing scope that contains the name. In the OP's example, as illustrated by the equivalent transformation shown above, the nearest scope that contains a declaration of the name swap is the namespace ns. The global scope will not be searched, std::swap will not be found via pure unqualified lookup.

Argument-dependent lookup searches all scopes (here: only namespaces and classes) associated with the argument types. For class types, the namespace in which the class has been declared in is an associated scope. Types of the C++ Standard Library such as std::vector<int> are associated with namespace std, hence std::swap can be found via argument-dependent lookup for the expression swap(a, b) if T is a C++ Standard Library type. Similarly, your own class types allow finding a swap function in the namespaces they have been declared in:

namespace N2 {
    class MyClass {};
    void swap(MyClass&, MyClass&);
}

Therefore, if argument-dependent lookup does not find a better match than pure unqualified lookup, you'll end up calling ns::swap recursively.


The idea behind calling swap unqualified, that is, swap(a, b) instead of std::swap(a, b) is that functions found via argument-dependent lookup are assumed to be more specialized than std::swap. Specializing a function template such as std::swap for your own class template type is impossible (since partial function template specializations are forbidden), and you may not add custom overloads to namespace std. The generic version of std::swap is implemented typically as follows:

template<typename T>
void swap(T& a, T& b)
{
    T tmp( move(a) );
    a = move(b);
    b = move(tmp);
}

This requires a move-construction plus two move-assignments, which might even fall back to copies. Therefore, you can provide a specialized swap function for your own types in the namespaces associated with those types. Your specialized version can make use of certain properties of, or private access to, your own types.

Heterodox answered 4/5, 2015 at 15:9 Comment(4)
You were both faster and more detailed, +1.Bola
@Angew I probably just started earlier ;)Heterodox
I just thought overloading std::swap for custom types is standard practice.Belmonte
@Belmonte Programs may not add definitions or declarations to the std namespace "unless otherwise specified" [namespace.std]p1. There is no such exception for std::swap. The common practice I know of is to define swap as a non-member friend function, possibly defined inside the class body. See also stackoverflow.com/a/5695855 Types defined by the C++ Standard Library do overload std::swap, but they're in the unique position of being part of the Standard - and std is the namespace they're associated with (so this follows the aforementioned common practice).Heterodox
B
2

The most important piece of the standard is 7.3.4/2 (quoting C++14 n4140, emphasis mine):

A using-directive specifies that the names in the nominated namespace can be used in the scope in which the using-directive appears after the using-directive. During unqualified name lookup (3.4.1), the names appear as if they were declared in the nearest enclosing namespace which contains both the using-directive and the nominated namespace.

The using-directive is located inside a function in :: ns, and nominates :: std. This means that for the purpose of unqualified name lookup, the effect of this using-directive is that names in ::std behave as if they were declared in ::. Particularly, not as if they were in ::ns.

Because the unqualified name lookup begins inside a function in ::ns, it will search ::ns before looking into ::. And it finds ::ns::swap, so it ends there, without examining ::, where it would find ::std::swap brought in by the using-directive.

Bola answered 4/5, 2015 at 15:9 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.