PSA: ADL requires that unqualified lookup has found a function
As seen on the cpplang Slack (hat tip to Jody Hagins).
Recall my post “What is the std::swap
two-step?” (2020-07-11), where I said:
A qualified call like
base::frotz(t)
indicates, “I’m sure I know how tofrotz
whatever this thing may be. No typeT
will ever know better than me how tofrotz
.”An unqualified call using the two-step, like
using my::xyzzy; xyzzy(t)
, indicates, “I know one way toxyzzy
whatever this thing may be, butT
itself might know a better way. IfT
has an opinion, you should trustT
over me.”An unqualified call not using the two-step, like
plugh(t)
, indicates, “Not only should you trustT
over me, but I myself have no idea how toplugh
anything. TypeT
must come up with a solution; I offer no guidance here.”
Several places in the C++20 STL use that third approach. For example, std::ranges::begin
tries an unqualified call to begin(t)
; if begin(t)
is ill-formed, std::ranges::begin
does not
fall back to std::begin
. Likewise, std::strong_order
tries an unqualified call to strong_order(t, t)
,
without keeping any_other::strong_order
as a fallback. And it’s not just C++20’s CPOs that use this pattern;
the constructor of C++11’s std::error_code
relies on an unqualified call to make_error_code(t)
.
But if you’re expecting your library’s unqualified call to f(t)
to trigger ADL, you must contend
with [basic.lookup.argdep]/1, which says that if
your initial unqualified lookup of f
finds a non-function (or a function declaration
in a block scope) then ADL doesn’t take place. If unqualified lookup finds a function, or a function template,
or nothing at all, then you get ADL; if it finds a non-function, you get nothing!
As of this writing, libc++ (but not libstdc++ or MSVC) can be tricked into stumbling on make_error_code
(Godbolt):
int make_error_code; // ha ha!
#include <system_error>
namespace N {
enum E { RED, YELLOW, BLUE };
std::error_code make_error_code(E);
}
template<>
struct std::is_error_code_enum<N::E> : std::true_type {};
int main() {
std::error_code e = N::E();
}
The compiler complains:
include/c++/v1/system_error:330:22: error: called object type 'int'
is not a function or function pointer
{*this = make_error_code(__e);}
^~~~~~~~~~~~~~~
note: in instantiation of function template specialization
'std::error_code::error_code<N::E>' requested here
std::error_code e = N::E();
^
Also as of this writing, libc++ and libstdc++ (but not MSVC) can be tricked into stumbling on strong_order
(Godbolt):
int strong_order; // ha ha!
#include <compare>
namespace N {
struct S {
friend auto strong_order(S, S) { ~~~ }
};
}
auto x = std::strong_order(N::S(), N::S());
The compiler complains:
error: no matching function for call to object of type
'const __strong_order::__fn'
auto x = std::strong_order(N::S(), N::S());
^~~~~~~~~~~~~~~~~
__compare/strong_order.h:120:56: note: candidate template ignored:
constraints not satisfied [with _Tp = N::S, _Up = N::S]
decltype(auto) operator()(_Tp&& __t, _Up&& __u) const
^
__compare/strong_order.h:124:44: note: because
'std::forward<_Tp>(__t) <=> std::forward<_Up>(__u)' would be invalid:
invalid operands to binary expression ('N::S' and 'N::S')
std::forward<_Tp>(__t) <=> std::forward<_Up>(__u);
^
__compare/strong_order.h:131:21: note: and
'strong_order(std::forward<_Tp>(__t), std::forward<_Up>(__u))' would be invalid:
called object type 'int' is not a function or function pointer
strong_order(std::forward<_Tp>(__t), std::forward<_Up>(__u));
^
The strong_order
found by unqualified lookup is the evil user’s int strong_order
variable, so Clang is quite right: strong_order
is not callable, and no ADL
takes place.
Hui Xie points out that you can break things even harder by making the evil declaration a namespace declaration:
namespace strong_order {}
namespace make_error_code {}
#include <system_error>
and that Boost.Graph is a non-STL library containing at least one example of this problem (Godbolt):
namespace target {}
#include <boost/graph/graph_concepts.hpp>
Most Ranges CPOs, such as ranges::begin
, specify that the ADL lookup is
“performed in a context in which unqualified lookup for begin
finds…” some
specific function declarations. In the case of ranges::begin
, those are:
void begin(auto&) = delete;
void begin(const auto&) = delete;
The primary motivation for these “poison pill” declarations (as I understand it)
is to prevent overload resolution from considering std::begin
as a candidate
via ordinary unqualified lookup (even though we would ordinarily expect a lookup
starting in namespace std::ranges
to find std::begin
). A secondary effect
(and the only reason, as far as I know, that motivates the exact signature void begin(auto&)
)
is to prevent overload resolution from considering “insufficiently constrained”
user-defined templates such as Bad
’s friend below (Godbolt):
struct OK {
friend int *begin(OK&);
} ok;
auto x = std::ranges::begin(ok); // OK
struct Bad {
friend int *begin(auto&);
} bad;
auto y = std::ranges::begin(bad); // ERROR!
But for this blog post’s purposes, the third and most important effect of a “poison pill” declaration is to make sure that unqualified lookup finds a function declaration — instead of searching all the way out to the global scope where an evil user might have declared a non-function with that name, thus preventing ADL.
Library implementors take note: bare unqualified ADL is probably a bad idea!
If you’re not using the std::swap
two-step, then use a “poison pill” declaration
to ensure that your unqualified lookup never finds a non-function.
See also:
-
“Why is deleting a function necessary when you’re defining a CPO?” on StackOverflow (Barry Revzin, August 2020)
-
microsoft/STL #1374: “Spaceship CPO wording in [cmp.alg] needs an overhaul”
-
LWG3629 “
make_error_code
andmake_error_condition
are customization points”