How to use ADL in Constraints?
Asked Answered
B

2

8

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?

Braunite answered 7/11, 2020 at 0:23 Comment(0)
O
2

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.

Oracle answered 7/11, 2020 at 1:15 Comment(4)
This looks like my answer but with more steps. Assuming the user enables isinf ADL for his type, what is the benefit of using this method over my suggestion?Redfaced
@Redfaced It gives you an object N::isinf that you can use to do the dispatch. And since it's an object, you can do things like pass it into algorithms.... so like ranges::any_of(r, N::isinf) works.Oracle
Hmm, that's correct. It's a slight benefit of not having to write [](auto t) { return adl_isinf(t); } at the cost of more complexity. Depends on what the user needs more. godbolt.org/z/KEbjGGRedfaced
Thanks for this answer. This is way more lines of code than I would have preferred, but at least it keeps things simple in the rest of the code. As a side note, the semicolon in the compound requirement on line 12 should be moved to the end of the line according to gcc and en.cppreference.com/w/cpp/language/constraintsBraunite
R
0

This should work. I'm not that comfortable with c++20 concepts so I cannot guarantee that the syntax is correct.

namespace impl {

using std::isinf;
template<class T>
auto adl_isinf(T t) -> decltype(isinf(t));
// { return isinf(t); } // implementation not necessary depending on whether
                        // requires is an unevaluated context
}

template <typename T>
concept Test = requires (T a) {
    impl::adl_isinf(a);
};
Redfaced answered 7/11, 2020 at 0:55 Comment(3)
This looks basically like the OP's second workaround to me.Grouchy
Yes, it is. The difference of defining the concept in the main namespace instead of importing it with a using statement might be enough to remove some of the "weirdness" OP is talking about though.Redfaced
Yes, I prefer your solution over my second workaround. I tested your code and the syntax is correct, but you wrote using std::inf instead of using std::isinf in line 3. I can't edit your answer since the change would be less than 6 characters.Braunite

© 2022 - 2024 — McMap. All rights reserved.