Search code examples
c++c++20c++-conceptsargument-dependent-lookup

How to use ADL in Constraints?


As an example, I want to use a constraint to ensure that the function isinf is implemented for a template parameter T. If T is one of float, double, long double or an integral type, this can be done in the following way:

#include <cmath>

template <typename T>
concept Test = requires (T a) {
    std::isinf(a);
};

However, I also want to use this constraint for custom, non-standard data types for which I implement my own isinf function. This function is not contained in the std namespace, so I tried the following instead:

#include <cmath>

template <typename T>
concept Test = requires (T a) {
    using std::isinf;
    isinf(a);
};

This does not work since every statement in the requires clause should be a valid requirement and using std::isinf is not a "requirement" at all.

I see two workarounds to this problem:

  • Move the using std::isinf; clause to the global namespace. But this introduces isinf to the global namespace, which I'd like to avoid.

  • Encapsulate the using std::isinf; clause with the concept definition in a namespace named ABC, then add using ABC::Test; directly after the namespace. This seems a little bit weird.

Is there a better solution?


Solution

  • The way this sort of thing works in Ranges is by creating a Customization Point Object. This closely mirrors your second option (we stick a using-declaration in a custom namespace) except we also provide an mechanism for users to call the correct isinf without having to write a bunch of the same kind of boilerplate themselves.

    A customization point object for isinf would look something like this:

    namespace N {
        // make our own namespace
        namespace impl {
            // ... where we can bring in std::isinf
            using std::isinf;
    
            struct isinf_t {
                // our type is constrained on unqualified isinf working
                // in a context where std::isinf can be found
                template <typename T>
                    requires requires (T t) {
                        { isinf(t) } -> std::same_as<bool>;
                    }
                constexpr bool operator()(T t) const {
                    // ... and just invokes that (we know it's valid and bool at this point)
                    return isinf(t);
                }
            };
        }
    
        // we provide an object such that `isinf(x)` incorporates ADL itself
        inline constexpr auto isinf = impl::isinf_t{};
    }
    

    And now that we have an object, a concept follows directly:

    template <typename T> 
    concept Test = requires (T t) {
        N::isinf(t);
    }
    

    This is precisely how the range concept is specified.