Unqualified lookup of operators in standard library templates
Asked Answered
U

1

6
namespace N {
    struct A {};
    
    template<typename T>
    constexpr bool operator<(const T&, const T&) { return true; }
}

constexpr bool operator<(const N::A&, const N::A&) { return false; }

#include<functional>

int main() {
    static_assert(std::less<N::A>{}({}, {}), "assertion failed");
}

See https://godbolt.org/z/vsd3qfch6.

This program compiles on seemingly random versions of compilers.

The assertion fails on all versions of MSVC since v19.15, but succeeds on v19.14.

It succeeds on GCC 11.2 and before, but fails on current GCC trunk.

It fails on Clang with libstdc++ in all versions. It succeeds with libc++ in all versions, including current trunk, except version 13.

It always succeeds with ICC.


Is it specified whether or not the static_assert should succeed?


The underlying issue here is that std::less uses < internally, which because it is used in a template will find operator< overloads via argument-dependent lookup from the point of instantiation (which is the proper method), but also via unqualified name lookup from the point of definition of the template.

If the global overload is found, it is a better match. Unfortunately this make the program behavior dependent on the placement and order of the standard library includes.

I would have expected that the standard library disables unqualified name lookup outside the std namespace, since it cannot be relied on anyway, but is that supposed to be guaranteed?

Uncloak answered 5/3, 2022 at 15:11 Comment(9)
MSVC (VS2019) is happy with your code if you add #include <functional>. (As are all the compilers shown in your linked Compiler Explorer page!)Kerato
@AdrianMole The include is there in the middle. The point of this question is that the placement of that include seems to matter and whether it should. Imagine that everything I put above the #include <functional> is part of a header file not using any standard library includes. If #include <functional> is put at the beginning then I am pretty sure that the assertion is guaranteed to succeed.Uncloak
It is always good practice to include STL headers before your own. Why would you want to do the opposite?Douce
@Uncloak Oops. Sorry, I missed that 'hidden' include statement. ;(Kerato
@WaisKamal I didn't want to complicate the example, but you can also imagine a variation in which the static_assert is not using A directly, but is together with the <functional> include in another header file as a template instantiated in main. Then standard library headers can be at the top of each header, while the result will be dependent on the inclusion order of the two headers in main.cpp.Uncloak
Exclude std altogether and make your own less ( template<typename _Tp> struct less { constexpr bool operator()(const _Tp& __x, const _Tp& __y) const { return __x < __y; } };) it succeeds at the top before <'s and fails after <'s.Motherly
@Motherly Yes and if the less is in the global namespace there is nothing to prevent that. However, if less is not in the global namespace, then the dependence on the location could be avoided by declaring an overload of operator< with hidden, non-constructible types as parameters before less in the same namespace. Then it wouldn't matter where they are placed (together). Question is why the standard library doesn't do this.Uncloak
Oh, why it doesn't do namespace test { class X; constexpr bool operator<(const X&, const X&); template<typename _Tp> struct less { constexpr bool operator()(const _Tp& __x, const _Tp& __y) const { return __x < __y; } }; } Presumably because they expect that the standard library headers are put at the top.Motherly
@Motherly Putting standard library includes at the top doesn't generally prevent this from happening if it happens inside generic code, see my comments above. This is usually not a problem because the operator overloads in the namespace of the type are specific to it, but this can cause issues if these overloads aren't that constrained.Uncloak
D
2

What matters here is whether the unqualified lookup from inside std finds any other operator< (regardless of its signature!) before reaching the global namespace. That depends on what headers have been included (any standard library header may include any other), and it also depends on the language version since C++20 replaced many such operators with operator<=>. Also, occasionally such things are respecified as hidden friends that are not found by unqualified lookup. It’s obviously unwise to rely on it in any case.

Dredge answered 5/3, 2022 at 16:30 Comment(4)
My question is whether the standard library is required to make the behavior consistent by always inserting a overload that prevents unqualified name lookup from reaching the global namespace. I take it that your answer implies that it isn't?Uncloak
@user17732522: Right: [global.functions]/4 is, I think, the only guarantee like that, but it’s for ADL and excludes operator functions anyway.Dredge
Do you know of any particular reason not to require it? I don't see any hindrance to implementing such an extra overload (with parameters making it unviable for all user calls) and I also don't see any ill effect, but it would avoid global operator overloads having such unintended consequences.Uncloak
@user17732522: I suspect it’s merely “your operators should all be found by ADL”, since there are lots of circumstances where they won’t work otherwise. That doesn’t mean a proposal to “prevent global namespace lookup” (with the understanding that it really meant such an unusable overload was provided as needed) would be rejected.Dredge

© 2022 - 2024 — McMap. All rights reserved.