N3490=12-0180
2012-10-31
Dave Abrahams
Controlling Argument-Dependent Lookup
Abstract:
In this paper, I present the two competing ideas about the nature of the ADL problem, identify one as important, and propose a feature that addresses it. I also propose an extension to that feature that extends proposal N3418's unification of functions, function templates, and function objects.
The Two Views
Previous attempts to "fix ADL" have centered around two proposals. The first, Explicit Namespaces by Dave Abrahams (full disclosure: me) sought to
The other one, A Modest Proposal: Fixing ADL by Herb Sutter, sought to change the rules of ADL so that it applies in fewer situations.
These two views were the source of ongoing confusion and disagreement until the Indiana committee meeting of August 2011, where Herb and I discussed the issue and realized we were solving different perceived problems.
Herb's Scenario
The scenario Herb's proposal addresses is simple to describe, so I'll start with that one. Two conditions hold
So far, I have not been able to find any evidence (not even the anecdotal kind) that this scenario is a real problem for anyone. Therefore, I'll let Herb argue for its importance, and move on to describe the problem I'm solving.
First, though, I note that changing the rules of ADL will break code, some of it silently. It is my understanding that in some cases the specific proposed change disables a technique currently used intentionally by libraries. Therefore, I suggest that before we take such an approach, we should be very sure that the costs of code breakage are more than balanced by real benefits.
My Scenario
My proposal addresses the following scenario:
This problem can arise in various circumstances, but the prototypical example looks something like this:
namespace A
{
void helper(some_type const& x);
void g(some_type x)
{
helper(x);
}
}
The problem only manifests when a namespace associated with some_type acquires its own helper. For example,
namespace B
{
class some_type { ... };
// Someone adds this overload
void helper(some_type& x);
}
If the programmer is lucky, she'll get a compiler error, but in this case she doesn't: B::helper silently "hijacks" the call made in A::g, because it is a slightly better match.
Whose Fault Is It?
Theory 1: the author of A messed up. The argument for this theory is that the author of A should have known that any unqualified call is subject to ADL, and should therefore have called A::helper rather than simply helper.
While this argument is technically correct, I have a hard time blaming the author of A, for several reasons:
Theory 2: the author of B messed up. After all, she knew that ADL was a problem, so she should have put some_type and helper in separate namespaces.
This argument, too, I find unconvincing. Creating special namespaces just to avoid ADL problems is tedious and ugly, and once again, to avoid the problem you have to learn about ADL and always keep it in mind. Furthermore, the author of B may actually intend helper to be called (but only called intentionally) via ADL.
Is It Real?
Is this a real problem? Yes. In fact, even standard library authors, who are aware of the issue and are among our best C++ programmers, frequently use unqualified calls where they don't intend to activate ADL (references upon request).
Does it break code often? Probably not. However, the fact that the problem manifests infrequently just means there is less awareness: programmers are less likely to know how to avoid it or even how to fix it.
Can we solve it with more awareness? I don't think so. The better you understand this problem, the less fun it is to program in C++. Personally, I spend an inordinate amount of mental energy, which should go into solving real problems, trying to avoid writing code that could break in the field due to ADL.
Therefore...
It should be fixed in the language.
Proposed Changes
First, we provide a way to turn off ADL for non-operator names. I propose the following syntax, which affects the scope in which it is written, for the rest of the translation unit:
using = delete;
| Note: if we wanted a way to turn off ADL for operators as well, we could use
| using operator = delete;
| but I'm not sure this capability is useful
Then, we provide a way to turn on ADL selectively, for a given name. To enable ADL for foo in the current scope, I propose an unqualified-using-declaration:
using foo;
Here's an example of how this capability might be used:
namespace algos
{
using = delete;
template <class RandomAccessIterator>
void reverse(RandomAccessIterator b, RandomAccessIterator e)
{
using swap; // find swap via ADL
if (b == e) return;
while(--e > b)
swap(*e, *b++);
}
}
The first unqualified-using-declaration in any scope turns off ADL for non-operator names, so that using = delete isn't needed. So:
void crazy_sort(std::vector<X>& v)
{
// using = delete; <=== Not needed
using swap; // find swap, and only swap, via ADL
using wiggle; // find swap and wiggle via ADL
...
}
Lastly, as a way to extend the unification of function objects with functions, I propose that an unqualified-using-declaration should enable ADL to find not only functions, but function objects in associated namespaces. For example:
namespace X
{
class Foo {...};
auto bar = [](Foo a) { return a.bar(); }
}
namespace Y
{
using bar; // Find bar, including function objects, via ADL
int value = bar(X::Foo()); // OK
}
Conclusion: Whither Modularity?
I define "modularity" as the ability to make changes in one part of the program without breaking the other parts. By this definition, ADL significantly weakens the modularity guarantees that namespaces were intended to offer (see D&E §3.12, Relationship to Classic C). The unqualified-using-declaration, along with using = delete, allow us to reclaim modularity without losing the elegance of ADL where we really want it.