Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

What is the difference between std::invocable and std::regular_invocable concepts?

What is the difference between std::invocable and std::regular_invocable? Based on the description from https://en.cppreference.com/w/cpp/concepts/invocable I would expect that the std::regular_invocable concept doesn't allow to change the state of of the function object when it is called (or at least the result of the calling should always return the same result).

Why the code below compiles?

Compiled with a command: g++-10 -std=c++2a ./main.cc.

#include <iostream>
#include <concepts>

using namespace std;

template<std::regular_invocable F>
auto call_with_regular_invocable_constraint(F& f){
    return f();
}

template<std::invocable F>
auto call_with_invocable_constraint(F& f){
    return f();
}

class adds_one {
    int state{0};
public:
    int operator()() { 
        state++;
        return state; 
    }
};

int main()
{
    auto immutable_function_object([]() { return 1; });
    adds_one mutable_function_object;

    // I would expect only first three will be compiled and the last one will fail to compile because the procedure is
    // not regular (that is does not result in equal outputs given equal inputs).
    cout << call_with_invocable_constraint(immutable_function_object) << endl;
    cout << call_with_invocable_constraint(mutable_function_object) << endl;
    cout << call_with_regular_invocable_constraint(immutable_function_object) << endl;
    cout << call_with_regular_invocable_constraint(mutable_function_object) << endl; // Compiles!
}

Output of the program:

1
1
1
2
like image 911
Wojciech Rembelski Avatar asked Mar 01 '23 22:03

Wojciech Rembelski


1 Answers

From the reference:

Notes

The distinction between invocable and regular_invocable is purely semantic.

This means that there is no way for the compiler to enforce the distinction through the concepts system, since that can only check syntactic properties.

From the introduction to the concepts library:

In general, only the syntactic requirements can be checked by the compiler. If the validity or meaning of a program depends whether a sequenced of template arguments models a concept, and the concept is satisfied but not modeled, or if a semantic requirement is not met at the point of use, the program is ill-formed, no diagnostic required.

Hypothetically, we could write:

template< class F, class... Args >
concept regular_invocable = invocable<F, Args...> &&
  requires(F&& f, Args&&... args) {
    auto prev = f;
    std::invoke(std::forward<F>(f), std::forward<Args>(args)...);
    assert(f == prev);
    // TODO assert that `args` are unchanged
    // TODO assert that invoking `f` a second time gives the same result
  };

However, this would not actually test that the assertion holds, since a requires clause is not invoked at run time but only checked at compile time.

like image 122
ecatmur Avatar answered Apr 19 '23 22:04

ecatmur