7

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

2 Answers2

8

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.

ecatmur
  • 152,476
  • 27
  • 293
  • 366
  • So, does it effectively mean that `regular_invocable` concept does not guarantee that `F` is regular? It is a bit confusing, since before they wrote "The regular_invocable concept adds to the invocable concept by requiring the invoke expression to be equality preserving" – pptaszni Nov 08 '20 at 14:53
  • Wouldn't for your example `f` be the function and `assert(f == prev);` check if the function stays the same instead of checking if it would return the same value? – t.niese Nov 08 '20 at 14:55
  • 4
    @pptaszni yes, it *requires* it but doesn't *guarantee* it. It's a little like being able to put `[[attribute(pure)]]` on a function declaration/definition. Perhaps in future compilers will be able to help check these requirement, but for now it's on the programmer to get it correct. – ecatmur Nov 08 '20 at 15:02
  • @t.niese yes, I also need to check that function arguments are unchanged - I'll put a note in that the assertions are incomplete – ecatmur Nov 08 '20 at 15:03
2

regular_invocable tells the user of the function that it will assume, that the result calling that regular_invocable function with the same value of the arguments will result in the same return value, and might cache that result due to that.

Caching the result could either be done by the function that expects regular_invocable or the compiler could use that information to optimize away multiple function calls to the regular_invocable function when the value of the arguments stay the same. So right now it can be seen as documentation and compiler hint.

Similar to const_cast it might not always be possible for the compiler to check if it is valid. Due to that and because there is currently not attribute/keyword in the standard to mark a function to always return the same value, there is right now no way to enforce at compile time that the function passed regular_invocable really matches that requirement.

t.niese
  • 39,256
  • 9
  • 74
  • 101