Confusion around function call resolution

2019-04-07 12:03发布

问题:

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.

回答1:

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.



回答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.