4
template<typename T>    concept A = true;
template<typename... T> concept B = A<T...>;

Clang and gcc complains 'pack expansion used as argument for non-pack parameter of concept' at the 2nd line. MSVC compiles. Are the code well-formed or not?


Here is a more concrete example showing why the question matters. Say we want to define a concept with parameter pack called single_integral_or_all_floating_point. It is true if the pack has single integral type or all floating point types. It is false otherwise.

template<typename... T>
concept single_integral_or_all_floating_point =
    std::is_integral<T...>::value ||
    (... && std::is_floating_point<T>::value);

If sizeof...(T) is not 1, there is a substitution failure on std::is_integral and the 1st constraint is not satisfied.

Let's say we want to get rid of the old type traits and switch to concepts. Clang and gcc do not compile with a drop-in replacement like below because of 'pack expansion used as argument for non-pack parameter of concept'. MSVC compiles.

template<typename... T>
concept single_integral_or_all_floating_point =
    std::integral<T...> || // error: pack expansion used as argument for non-pack parameter of concept
    (... && std::floating_point<T>);

We have to come up a workaround for clang/gcc.

template<typename... T>
struct only {};

template<typename T>
struct only<T> {
    using type = T;
};

template<typename... T>
concept single_integral_or_all_floating_point =
    std::integral<typename only<T...>::type> ||
    (... && std::floating_point<T>);

So if clang and gcc are correct, it is pretty awkward to use concepts in the similar cases.

wanghan02
  • 1,227
  • 7
  • 14
  • 1
    You have a two different questions in one, and the first one is moreover extremely bloated, which may be the reason you haven't gotten any attention at all to this question. Consider removing all the noise and rewriting the whole question into 3 lines, namely [this 2-line minimal demo](https://godbolt.org/z/4PhxGj63n) and a third line with a question whether the demo is well-formed or not (given that MSVC accepts it whereas Clang and GCC does not). The second question could be posted as a separate follow-up question after you've gotten the answer to the first one. – dfrib Jan 09 '23 at 14:54
  • It might be *too* minimal now, as it's unclear (well, to me) what are you trying to do (something like [this](https://godbolt.org/z/8Mf7v4WT5)?) with the pack. – Bob__ Jan 09 '23 at 15:37
  • @Bob__ Thanks. I have added the 2nd example back. I hope it's clear enough. – wanghan02 Jan 09 '23 at 15:59
  • 1
    You might use another template parameter: https://godbolt.org/z/54Yx3njhr – Bob__ Jan 09 '23 at 16:22
  • @Bob__ Yes, there are always workarounds available. But the workaround is not as canonical as the one in the example if the expansion is allowed. Besides, your example doesn't compile if there are no types (I think all of with empty set should be true). – wanghan02 Jan 09 '23 at 16:54
  • @Bob__ Actually the type list could be empty does make the concept implementation more difficult if the expansion is not allowed. A helper concept/struct is always needed. – wanghan02 Jan 09 '23 at 17:15
  • 1
    I see. I'd probably still use a trait as helper: https://godbolt.org/z/5xanTM65M . To my knowledge, concepts are "limited" by design (see e.g. https://stackoverflow.com/questions/70828764/why-cant-we-specialize-concepts). – Bob__ Jan 09 '23 at 20:31
  • 1
    You may want to consider tagging this as [tag:language-lawyer] for better visibility, since it's asking a question about whether a very specific use-case of code is well-formed as according to said standard – Human-Compiler Jan 12 '23 at 13:53
  • @wanghan02 your work-around might fail to satisfy your own constraint, it allows for mixed integral with floating point types, but it sounds like you only want to satisfy the concept if there is 1 integral type or N floating point types. – Substitute Jan 13 '23 at 17:19

1 Answers1

2

The main issue with your first snippet is that concepts must be able to be normalized, and normalization requires a parameter mapping of each template parameter of an atomic constraint. The fact that the prototype parameter of A is not used in its constraint seems to be irrelevant to GCC/Clang; they apparently attempt to create some kind of temporary mapping anyway, which fails since a non-pack cannot map to a pack. Which makes sense IMO, because that would be incompatible in whatever internal system normal forms are expressed in.

Regarding your broader use case, there are certainly more sensible ways to express your constraint, e.g. explicitly checking sizeof.... You could also use a variable template, whose template-id would be an atomic constraint and thus not prone to the same problem:

template<typename... T>
concept single_integral_or_all_floating_point =
    std::is_integral_v<T...> ||
    (... && std::floating_point<T>);

Demo.

Columbo
  • 60,038
  • 8
  • 155
  • 203