10

PSA: ADL requires that unqualified lookup has found a function

 2 years ago
source link: https://quuxplusone.github.io/blog/2022/04/14/another-reason-for-the-poison-pill/
Go to the source link to view the article. You can view the picture content, updated content and better typesetting reading experience. If the link is broken, please click the button below to view the snapshot at that time.
neoserver,ios ssh client

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 to frotz whatever this thing may be. No type T will ever know better than me how to frotz.”

An unqualified call using the two-step, like using my::xyzzy; xyzzy(t), indicates, “I know one way to xyzzy whatever this thing may be, but T itself might know a better way. If T has an opinion, you should trust T over me.”

An unqualified call not using the two-step, like plugh(t), indicates, “Not only should you trust T over me, but I myself have no idea how to plugh anything. Type T 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:


About Joyk


Aggregate valuable and interesting links.
Joyk means Joy of geeK