c++c++17sfinaeargument-dependent-lookupglobal-scope

How to check at compile time for the existence of a global-scope function accepting given argument types?


What (I think) I need

How can I define a type trait that checks whether, for a type T, the function ::foo(T) is declared?

What I'm finding hard, is to have ::foo in SFINAE friendly way. For instance, if the compiler has got to the point where the following is defined,

template<typename T>
void f(T t) { foo(t); }

it's prefectly fine if no foo whatsover has been seen so far.

But as soon as I change foo to ::foo, then I get a hard error.

The use case (in case you think I don't need the above)

I have a customization point like this:

// this is in Foo.hpp
namespace foos {
inline constexpr struct Foo {
    template <typename T, std::enable_if_t<AdlFooable<std::decay_t<T>>::value, int> = 0>
    constexpr bool operator()(T const& x) const {
        return foo(x);
    }
} foo{};
}

that allows customizing the behavior of a call to foos::foo by defining an ADL foo overload for the desired type.

The definition of the trait is straightforward:

// this is in Foo.hpp
template <typename T, typename = void>
struct AdlFooable : std::false_type {};

template <typename T>
struct AdlFooable<T, std::void_t<decltype(foo(std::declval<T const&>()))>>
    : std::true_type {};

Given that, if one defines

// this is in Bar.hpp
namespace bar {
    struct Bar {};
    bool foo(bar::Bar);
}

then a call to

// other includes
#include "Foo.hpp"
#include "Bar.hpp"
bool b = foos::foo(bar::Bar{});

works as expected, with foos::foo routing the call to bar::foo(bar::Bar), which is found via ADL. And this works well regardless of the order of the two #includes, which is good, because they might be included in either order, if // other includes transitively includes them.

So far so good.

What I don't really like of this approach, is that one could mistakenly define, instead of the second snippet above, the following,

// this is in Bar.hpp
namespace bar {
    struct Bar {};
}
bool foo(bar::Bar);

with foo in global scope.

In this case, if the #include "Bar.hpp" happens to come before #include "Foo.hpp", the code program will work, because the body of Foo::operator() will pick ::foo(bar::Bar) by ordinary lookup.

But as soon as the order of the #includes happens to be reversed, then the code breaks.

Yes, the bug is in defining foo(bar::Bar) in global namespace, but I think it's also a bug that it can pass unnoticed by pure chance.

That's why I would like to change the type trait to express that "an unqualified call to foo(T) is found, but not via ordinary lookup", or, more directly, "foo(std::declval<T>()) must compile, but ::foo(std::declval<T>()) must not compile".

A non-solution for those who are curious

After I accepted the answer, I have re-read relevant parts of Josuttis' C++ Templates - The Complete Guide and found that ADL can be inhibited by a mean other than qualifying the function, i.e. wrapping the name of the unqualified function in parenthesis, e.g. while foo(args...) undergoes ADL, (foo)(args...) doesn't!

Unfortunately, that just means that the name foo is looked up as if it was fully qualified (I presume with the current namespace), and so the attempt fails the same way as described above.


Solution

  • The ordinary way to do this is to call the customization implementation only via ADL. So the overload ::foo(bar::Bar) is not considered in any case, regardless of include order.

    And the way to do this is to include a poison pill:

    namespace foos {
    namespace detail {
    void foo() = delete;
    struct Foo {
        template <typename T>
        constexpr auto operator()(T const& x) const -> decltype(foo(x)) {
            return foo(x);
        }
    };
    }
    inline constexpr detail::Foo foo{};
    }
    

    The ordinary lookup of foo will find foos::detail::foo which will not work. It cannot examine the global scope. Therefore, the foo call will only lookup via ADL.


    Edit:

    This actually tells you how to form a trait that foo(x) is only callable by ADL, the nominal question. And that is, if foos::foo(x) is callable with this implementation, then foo(x) is callable via ADL only.