Home > Software design >  Why does this requires expression work as a concept but not directly on a function?
Why does this requires expression work as a concept but not directly on a function?

Time:10-21

The following fails to compile.

template <typename... U>
requires requires(U... x) { (std::to_string(x) && ...); }
auto to_string(const std::variant<U...>& value) {
    return std::visit([](auto&& value) {
        return std::to_string(std::forward<decltype(value)>(value));
    }, value);
}

int main() {
    std::variant<int, float> v = 42;
    std::cout << to_string(v) << std::endl;
}

https://godbolt.org/z/Wvn6E3PG5

If I convert the direct requires expression into a concept though, it works fine.

template<typename T, typename... U>
concept to_stringable = requires(U... u) { (std::to_string(u) && ...); };

template <to_stringable... U>
auto to_string(const std::variant<U...>& value) {
    return std::visit([](auto&& value) {
        return std::to_string(std::forward<decltype(value)>(value));
    }, value);
}

int main() {
    std::variant<int, float> v = 42;
    std::cout << to_string(v) << std::endl;
}

https://godbolt.org/z/W6znbvTzo

CodePudding user response:

When you have:

template <to_stringable... U>
auto to_string(const std::variant<U0, U1>& value) {

This checks if each individual type satisfies to_stringable<T>, so it is essentially equivalent to:

template <to_stringable U0, to_stringable U1>
auto to_string(const std::variant<U0, U1>& value) {

And with just one argument T your concept is:

requires(T t) { (std::to_string(t)); };

However, with more than one argument, it looks like:

requires(T1 t1, T2 t2) { (std::to_string(t1) && std::to_string(t2)); }

Which doesn't work because you can't && two std::strings, so the constraint is not satisfied.

What you really want to do is fold over a constraint:

template <typename... U>
requires ((requires(U x) { std::to_string(x); }) && ...)

... Which gcc doesn't seem to implement properly because of a bug but you can get away with:

template <typename... U>
requires requires(U... x) { ((void) std::to_string(x), ...); }

CodePudding user response:

You did not exactly convert it into a concept. This would be an exact conversion into a concept:

template<typename... U>
concept real_to_stringable = requires(U... u) { (std::to_string(u) && ...); };

template <typename ...U>
   requires real_to_stringable<U...>
auto to_string(const std::variant<U...>& value) {
    return std::visit([](auto&& value) {
        return std::to_string(std::forward<decltype(value)>(value));
    }, value);
}

And it gives the same error. For the same reason: std::to_string returns a basic_string. Which does not have a && overload. And therefore, &&ing them all together doesn't compile. So the constraint is not satisfied.

The reason your version of the concept code appears to work is because you didn't do it right. A concept whose first parameter is a type is a "type concept". A type concept can be used in place of a typename in certain circumstances as a shorthand for adding a requires constraint.

The first template parameter for such a concept is filled in with the type given as an argument to the template being constrained. The other template parameters for the concept are filled in by the other arguments provided at the point of the concept's use. But... you didn't provide any; to_stringable wasn't given other arguments in the template header. So the U in your to_stringable is always an empty set. Which means the constraint is always satisfied.

That is, template<to_stringable ...U> produces a requires constraint equivalent to requires (to_stringable<U> && ...), not requires to_stringable<U...>.

  • Related