Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

C++ Concepts Lite: Short-circuiting in concept bodies

I am trying to learn about the Concepts Lite TS that is yet to be merged into the standard. I'm confused about the behavior of short-circuiting disjunctions in concept bodies.

Here is a small example:

#include <type_traits>
#include <iostream>

template <typename T, typename ... Ts> concept bool myconcept =
(sizeof...(Ts) == 0) || (std::is_same_v<T, std::common_type_t<Ts...>>);

template <typename ... Ts>
void myfunc(Ts ... args) requires myconcept<int, Ts...> {
    (... , (std::cout << args << std::endl));
}

int main() {
    myfunc();
    return 0;
}

Compiling with gcc 7.1 and -fconcepts, gives the error:

error: cannot call function 'void myfunc(Ts ...) requires  myconcept<int, Ts ...> [with Ts = {}]'

In this example, std::common_type_t<Ts...> does not exist since the struct std::common_type<Ts...> does not have a member type if Ts = {}. However, I think this should compile because cppereference.com's documentation on concepts and constraints states that

Disjunctions are evaluated left to right and short-circuited (if the left constraint is satisfied, template argument deduction into the right constraint is not attempted).

Since sizeof...(Ts) == 0 is satisfied, template argument deduction should not be attempted on the second constraint and the requirement myconcept<int, Ts...> should be satisfied.

Strangely, placing the requirements directly into the function declarator causes the program to compile:

#include <type_traits>
#include <iostream>

template <typename ... Ts>
void myfunc(Ts ... args) requires (sizeof...(Ts) == 0) || (std::is_same_v<int, std::common_type_t<Ts...>>) {
    (... , (std::cout << args << std::endl));
}

int main() {
    myfunc();
    return 0;
}

Is there a good explanation for this behavior? Thanks.

like image 765
bananasplits Avatar asked Oct 29 '22 05:10

bananasplits


1 Answers

The layman’s explanation that appears on cppreference is correct. Select wording from the n4674 draft is quite clear as well:

A conjunction is a constraint taking two operands. A conjunction of constraints is satisfied if and only if both operands are satisfied. The satisfaction of a conjunction’s operands are evaluated left-to-right; if the left operand is not satisfied, template arguments are not substituted into the right operand, and the constraint is not satisfied. […]

(From 17.10.1.1 Logical operations [temp.constr.op] §2.) Since all of the wording that precisely establish how we go from concepts and templates to the conjunction or disjunction of atomic constraints is quite long, we will stick to the layman’s explanation.

Is there a good explanation for this behavior? Thanks.

As of this writing the GCC implementation of concepts is very much experimental. As a workaround you can refactor the problematic part into its own concept:

template<typename T, typename... Ts>
concept bool refactored = std::is_same_v<T, std::common_type_t<Ts...>>;

template<typename T, typename... Ts>
concept bool myconcept = sizeof...(Ts) == 0 || refactored<T, Ts...>;

Coliru demo

like image 133
Luc Danton Avatar answered Nov 15 '22 05:11

Luc Danton