9

Background

We know that the concept std::same_as is agnostic to order (in other words, symmetric): std::same_as<T, U> is equivalent to std::same_as<U, T> (related question). In this question, I would like to implement something more general: template <typename ... Types> concept same_are = ... that checks whether types in the pack Types are equal to each other.

My attempt

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

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

template <typename... Types>
concept are_same = (... && same_with_others<Types, Types...>);

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

// Note the order <U, T> is intentional
template< class T, class U> requires (are_same<U, T> && std::integral<T>)
void foo(T a, U b) {
    std::cout << "Integral" << std::endl;
}

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

(My intention here is to enumerate over every possible ordered pair of types in the pack)

Unfortunately, this code would not compile, with the compiler complaining that the call to foo(int, int) is ambiguous. I believe that it considers are_same<U, T> and are_same<T, U> as not equivalent. I would like to know why the code fails how I can fix it (so that the compiler treats them as equivalent)?

ph3rin
  • 4,426
  • 1
  • 18
  • 42
  • My gut tells me it's gonna need a helper that executes `same_with_others` in every possible permutation of the types. – StoryTeller - Unslander Monica Nov 06 '19 at 06:52
  • I'm not entirely sure if I understand you correctly. You want to check if _all_ types of `... Types` are the same? Maybe [std::conjunction](https://en.cppreference.com/w/cpp/types/conjunction) can help you. There is an example on the bottom of the page thats looks similiar to your approach. – Lukas-T Nov 06 '19 at 06:59
  • @StoryTeller-UnslanderMonica But I have already enumerated every possible ordered pair of types in the pack. Isn't that enough? Or compilers cannot determine the equivalence of folds without any concrete types? – ph3rin Nov 06 '19 at 07:02
  • @churill I mean to implement this in concepts, and parameter ordering needs special care in concepts. – ph3rin Nov 06 '19 at 07:04
  • I'm not sure, hence it's just a gut feeling. Could be the GCC devs aren't sure either yet. Could also be they haven't implemented it fully yet. – StoryTeller - Unslander Monica Nov 06 '19 at 07:05
  • @DanielLangr I mean to test concept subsumption relation by writing specialization using stricter constrains. (test whether `are_same` is equivalent to `are_same`) – ph3rin Nov 06 '19 at 07:11
  • `std::same_as` can only be implemented with magic (a.k.a. compiler help). So is your proposal, I suppose. – L. F. Nov 06 '19 at 09:32
  • It seems that fold expressions are not handled reasonably for constraint subsumption. This works: https://godbolt.org/z/lIjbH0, but this doesn't: https://godbolt.org/z/qIR4vH. Please submit a PR. – metalfox Nov 06 '19 at 09:38
  • @metalfox: From https://en.cppreference.com/w/cpp/language/constraints#Constraint_normalization, it seems expected. – Jarod42 Nov 06 '19 at 09:54

3 Answers3

6

From cppreference.com Constraint_normalization

The normal form of any other expression E is the atomic constraint whose expression is E and whose parameter mapping is the identity mapping. This includes all fold expressions, even those folding over the && or || operators.

So

template <typename... Types>
concept are_same = (... && same_with_others<Types, Types...>);

is "atomic".

So indeed are_same<U, T> and are_same<T, U> are not equivalent.

I don't see how to implement it :-(

Jarod42
  • 203,559
  • 14
  • 181
  • 302
6

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.

Barry
  • 286,269
  • 29
  • 621
  • 977
  • 2
    It is surprising to me that [this](https://godbolt.org/z/35YJAa) doesn't work. It is reasonable that only concepts can be subsumed. However, in my opinion, `(... && C)` not subsuming the *concept* `C` would surprise a lot of users. – metalfox Nov 11 '19 at 08:19
  • @metalfox: From my reading of normalization, your example should be fine (using constraint explicitly works though [Demo](https://godbolt.org/z/Z2-Wkh)). – Jarod42 Nov 12 '19 at 16:47
  • @Jarod42 What you wrote and what metalfox wrote are not the same thing - the difference is what metalfox is talking about. – Barry Nov 12 '19 at 16:54
  • @Jarod42 Yes, that works because [concepts participate in constraint subsumption](https://stackoverflow.com/a/52062387), and you have materialized the fold expression (involving concepts) into a single concept. Unfortunately, as you stated in your answer, fold expressions are not normalized into the concepts they are made of. This doesn't work either: https://godbolt.org/z/pjmKxR – metalfox Nov 12 '19 at 17:03
  • I might have misunderstood [Constraint_normalization](https://en.cppreference.com/w/cpp/language/constraints#Constraint_normalization) then :-/ I understand `((fold1 && ...) && (fold2 &&...))` as *conjunction* of `(fold1 && ...)` and `(fold2 && ...)` whereas it is atomic. – Jarod42 Nov 12 '19 at 17:22
  • 1
    @Jarod42 No, that's right. It is a conjunction. It's just that `(C && ...)` is atomic, so writing `(C && ...)` in two different places don't subsume each other. On the other hand, writing one `D` does. – Barry Nov 12 '19 at 17:43
  • Oh, I found the part I miss *"Only concepts can be subsumed"* from your link, which is from the part I probably misunderstand *"Two atomic constraints are considered identical if they are formed from the same expression at the source level and their parameter mappings are equivalent."*. Not yet understood `BadMeowableCat` from wording rules :-/ – Jarod42 Nov 12 '19 at 18:06
-1

churill is right .Using std::conjunction_v might be helpful.

   template <typename T,typename... Types>
   concept are_same = std::conjunction_v<std::same_as<T,Types>...>;