Why Does Same_As Concept Check Type Equality Twice

Why does same_as concept check type equality twice?

Interesting question. I have recently watched Andrew Sutton's talk on Concepts, and in the Q&A session someone asked the following question (timestamp in the following link):
CppCon 2018: Andrew Sutton “Concepts in 60: Everything you need to know and nothing you don't”

So the question boils down to: If I have a concept that says A && B && C, another says C && B && A, would those be equivalent? Andrew answered yes, but pointed out the fact the compiler has some internal methods (that is transparent to the user) to decompose the concepts into atomic logical propositions (atomic constraints as Andrew worded the term) and check whether they are equivalent.

Now look at what cppreference says about std::same_as:

std::same_as<T, U> subsumes std::same_as<U, T> and vice versa.

It is basically an "if-and-only-if" relationship: they imply each other. (Logical Equivalence)

My conjecture is that here the atomic constraints are std::is_same_v<T, U>. The way compilers treat std::is_same_v might make them think std::is_same_v<T, U> and std::is_same_v<U, T> as two different constraints (they are different entities!). So if you implement std::same_as using only one of them:

template< class T, class U >
concept same_as = detail::SameHelper<T, U>;

Then std::same_as<T, U> and std::same_as<U, T> would "explode" to different atomic constrains and become not equivalent.

Well, why does the compiler care?

Consider this example:

#include <type_traits>
#include <iostream>
#include <concepts>

template< class T, class U >
concept SameHelper = std::is_same_v<T, U>;

template< class T, class U >
concept my_same_as = SameHelper<T, U>;

template< class T, class U> requires my_same_as<U, T>
void foo(T a, U b) {
std::cout << "Not integral" << std::endl;
}

template< class T, class U> requires (my_same_as<T, U> && std::integral<T>)
void foo(T a, U b) {
std::cout << "Integral" << std::endl;
}

int main() {
foo(1, 2);
return 0;
}

Ideally, my_same_as<T, U> && std::integral<T> subsumes my_same_as<U, T>; therefore, the compiler should select the second template specialization, except ... it does not: the compiler emits an error error: call of overloaded 'foo(int, int)' is ambiguous.

The reason behind this is that since my_same_as<U, T> and my_same_as<T, U> does not subsume each other, my_same_as<T, U> && std::integral<T> and my_same_as<U, T> become incomparable (on the partially ordered set of constraints under the relation of subsumption).

However, if you replace

template< class T, class U >
concept my_same_as = SameHelper<T, U>;

with

template< class T, class U >
concept my_same_as = SameHelper<T, U> && SameHelper<U, T>;

The code compiles.

Why is std::same_as implemented in such a weird way?

It is t handle subsumption which only happens with concepts.

With your proposal,

same_as<T, U> doesn't subsume same_as<U, T>.

Further reading in cppreference.

How to implement the generalized form of std::same_as (i.e. for more than two type parameters) that is agnostic to parameter order?

The problem is, with this concept:

template <typename T, typename... Others>
concept are_same = (... && std::same_as<T, Others>);

Is that the normalized form of this concept is... exactly that. We can't "unfold" this (there's nothing to do), and the current rules don't normalize through "parts" of a concept.

In other words, what you need for this to work is for your concept to normalize into:

... && (same-as-impl<T, U> && same-as-impl<U, T>)

into:

... && (is_same_v<T, U> && is_same_v<U, T>)

And consider one fold-expression && constraint to subsume another fold-expression constraint && if its underlying constraint subsumes the other's underlying constraint. If we had that rule, that would make your example work.

It may be possible to add this in the future - but the concern around the subsumption rules is that we do not want to require compilers to go all out and implement a full SAT solver to check constraint subsumption. This one doesn't seem like it makes it that much more complicated (we'd really just add the && and || rules through fold-expressions), but I really have no idea.

Note however that even if we had this kind of fold-expression subsumption, are_same<T, U> would still not subsume std::same_as<T, U>. It would only subsume are_same<U, T>. I am not sure if this would even be possible.

How is constraint overloading resolved by the partial ordering of concept?

When compiler checks if one set of requirements is more constrained than another one, it recursively expands concepts. It also understands the meaning of &&, ||, ( ) (so the order of conjuncts/disjuncts doesn't matter, the superfluous ( ) don't matter, etc), even inside of concepts.

This part is pretty intuitive. What's not intuitive is that it's not enough for two requirements to be lexically same for them to be considered equivalent. They must literally be the same expression in the same location in the source code, at the point before concepts are expanded. This requires them to originate from the same concept.

Another non-intuitive part is && and || lose their special meaning in a fold expression, so for two fold expressions to be considered equivalent (whether they use && or || or something else) they too have to be in the same location in the source code before concepts are expanded.

Knowing this, the solution is to abstract away sizeof...(Ts) <= 3 and (same_as<Ts, int> && ...) into concepts.

There are numerous ways to do that. You can be as general or as specific as you want:


  1. template <typename ...P>
    concept at_most_3 = sizeof...(P) <= 3;

    template <typename ...P>
    concept all_ints = (std::same_as<P, int> && ...);

    Usage: requires at_most_3<Ts...> && all_ints<Ts...>


  2. template <auto A, auto B>
    concept less_eq = A <= B;

    template <typename T, typename ...P>
    concept all_same_as = (std::same_as<T, P> && ...);

    Usage: requires less_eq<sizeof...(Ts), 3> && all_same_as<int, Ts...>

Even the completely egregious template <bool X> concept boolean = X;, being used as requires boolean<sizeof...(Ts) <= 3> && boolean<(std::same_as<Ts, int> && ...)>, appears to work!

How can a clang 10 C++20 concept specify compound requirements for class methods?

The concepts proposal changed and now it requires using std::same_as.

This compiles fine with Clang 10 (although you may need to provide std::same_as yourself if you don't have a stdlib with it):

template <typename A>
concept MyConcept = requires(A a, bool b) {
{ a.one() } -> std::same_as<bool>;
a.two();
a.three(b);
};

struct SomeType {
bool one() { return true; }
void two() {}
void three(bool) {}
};

bool foo(MyConcept auto a) {
return a.one();
}

void bar() {
foo(SomeType());
}

Compare two sets of types for equality

If types in tuples are unique you could make use of inheritance to answer if all types from the first tuple are involved as a base of the helper struct. E.g. (C++11 approach):

#include <tuple>
#include <type_traits>

template <class T>
struct tag { };

template <class... Ts>
struct type_set_eq_helper: tag<Ts>... { };

template <class, class, class = void>
struct type_set_eq: std::false_type { };

template <bool...>
struct bool_pack { };

template <bool... Bs>
using my_and = std::is_same<bool_pack<Bs..., true>, bool_pack<true, Bs...>>;

template <class... Ts1, class... Ts2>
struct type_set_eq<std::tuple<Ts1...>, std::tuple<Ts2...>, typename std::enable_if< (sizeof...(Ts1) == sizeof...(Ts2)) && my_and< std::is_base_of<tag<Ts2>, type_set_eq_helper<Ts1...>>::value... >::value >::type >:
std::true_type { };

int main() {
using t1 = std::tuple<int, double>;
using t2 = std::tuple<double, int>;
using t3 = std::tuple<int, double, char>;

static_assert(type_set_eq<t1, t1>::value, "err");
static_assert(type_set_eq<t1, t2>::value, "err");
static_assert(!type_set_eq<t1, t3>::value, "err");
}

[Live demo]

Can concepts(C++20) be used as a boolean?

A concept-id like std::same_as<int, double> is evaluated like an expression. It produces a prvalue of type bool.

[temp.names]

8 A concept-id is a simple-template-id where the template-name is
a concept-name. A concept-id is a prvalue of type bool, and does not
name a template specialization. A concept-id evaluates to true if the
concept's normalized constraint-expression is satisfied
([temp.constr.constr]) by the specified template arguments and false
otherwise.

So decltype is reporting it correctly. In an expression, it's a bool.

How to check if two types come from the same templated class

Okay, how about this:

#include <iostream>

template<class T>
struct A {};
struct B {};

template <typename T>
struct should_reject { static constexpr bool value = false; };

template <typename T>
struct should_reject<A<T>> { static constexpr bool value = true; };

template <typename T>
void rejectA(T t)
{
std::cout << should_reject<T>::value << std::endl;
}

int main() {
rejectA(B()); // false
rejectA(1); // false
rejectA(1.0); // false
rejectA(A<B>()); // true
rejectA(A<int>()); // true
rejectA(A<double>()); // true
}

C++20 concept syntax - what does the requires parameter variables mean?

Why do they ask me to declare these variables like a or b? Why not even something like..

You aren't asked or required to use the variables. You are given the option because it's much more readable. There's little difference, as far as checking the constraints goes.

The reason the articles give examples like that one you cite, is because it's easier for humans to understand. The Equal concept is defined to read "given two hypothetical objects of this type, we can write a comparison expression with them, that results in a bool". That's it. You don't have to use this notation, you can write it with as much verbosity as you want.

But the old bit of wisdom about code still holds. Code is meant to be read far more than written, and not just in ways we foresee. When your concept is not satisfied in a context that results in a compiler error about a violated constraint, the compiler is likely to include the constraint in its error message. Which version of the concept is going to produce the easier-to-understand error message? My bet is on the one that uses helper variables.

How to make a variadic is_same?

Use template recursion:

template<typename T, typename... Rest>
struct is_any : std::false_type {};

template<typename T, typename First>
struct is_any<T, First> : std::is_same<T, First> {};

template<typename T, typename First, typename... Rest>
struct is_any<T, First, Rest...>
: std::integral_constant<bool, std::is_same<T, First>::value || is_any<T, Rest...>::value>
{};

static_assert(is_any<int, char, double, int>::value, "error 1"); // OK
static_assert(is_any<int, char, double, short>::value, "error 2"); // error


Related Topics



Leave a reply



Submit