C++ Concepts - Concept in the requires parenthesis causes 2 conflicting error messages

Viewed 487

I've worked hard this morning to understand concepts a bit more (still a newbie), and I've stumbled to something strange.

I wanted to understand how good concepts work with Lambda functions, I've tried toying around with them a lot.

Lets start with this code (that works!):

#include <concepts>
#include <stdio.h>

template <typename T>
concept Integral =
requires(T n)
{
    { n } -> std::convertible_to<int>;
};

template <typename Operation>
concept CalculatorOperation =
requires(Operation&& op, int a, int b)
{
    { op(a, b) } -> Integral;
};

template <CalculatorOperation Operation>
static inline constexpr Integral auto Calculator(const Integral auto First,
                                                 const Integral auto Second)
{
    return Operation{}(First, Second);
}

int main()
{
    static constexpr auto Multiply = [](const Integral auto First,
                                        const Integral auto Second)
                       { return First * Second; };
    return Calculator<decltype(Multiply)>(15, 18);
}

I created an Integral concept that (just for dummy's sake) is satisfied if a type is convertible to int. I've created a calculator operation concept that accepts a lambda, two ints, and makes sure the operation returns an Integral.

I decided that I want the int in the concept to turn into an Integral, since I don't want only int's to be accepted to the operation (I want all integral types).

So I changed

requires(Operation&& op, int a, int b)

to

requires(Operation&& op, Integral&& a, Integral&& b)

Not only did this not work for me, I actually got 2 conflicting error messages (on GCC 10.1):

<source>:13:30: error: placeholder type not allowed in this context


<source>:13:30: error: expected 'auto' or 'decltype(auto)' after 'Integral'

   13 |     requires(Operation&& op, Integral&& a, Integral&& b)

I ran this using GodBolt's website Compiler Explorer. Whats going on here? Is there an actual way to do this?

1 Answers

First off, by doing this:

requires(Operation&& op, Integral&& a, Integral&& b)

you probably actually meant this:

requires(Operation op, Integral auto a, Integral auto b)

that is, to use the type-constraint auto form of a placeholder type specifier.

However, auto parameters produce function templates, and the auto placeholder is an equivalent of an actual invented type template parameter of that function template, thus it can appear only in contexts where type deduction from an initializer takes place, and that means that:

void foo(auto x);

is an equivalent of:

template <typename T>
void foo(T x);

Similarly,

void bar(Integral auto x);

is an equivalent of:

template <Integral T>
void bar(T x);

Such transformation is not applicable to a requires expression, i.e., there's no initializer whose type could be deduced, and so one cannot use an auto placeholder in the parameter declaration of a requirement parameter list. These parameters, as the standard says, "are only used as notation for the purpose of defining requirements." So instead, you need to explicitly specify all template arguments that are needed for the concept to work, e.g.:

template <typename Operation, typename Lhs, typename Rhs>
concept CalculatorOperation = requires(Operation op, Lhs a, Rhs b)
{
    { op(a, b) } -> Integral;
};

template <typename Operation>
constexpr Integral auto Calculator(Integral auto first, Integral auto second)
    requires CalculatorOperation<Operation, decltype(first), decltype(second)>
{
    return Operation{}(first, second);
}

Also note that I removed && from each of the parameter declarations in the requirement parameter list. The whole requires expression is an unevaluated context, and no arguments are actually passed.

Related