std::invocable 和 std::regular_invocable 概念有什么区别?

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

std::invocablestd::regular_invocable 有什么区别?根据来自的描述 https://en.cppreference.com/w/cpp/concepts/invocable 我希望 std::regular_invocable 概念不允许在函数对象被调用时改变它的状态(或者至少调用的结果应该总是 return同样的结果)。

为什么下面的代码可以编译?

使用命令编译: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!
}

程序输出:

1
1
1
2

来自the reference

Notes

The distinction between invocable and regular_invocable is purely semantic.

这意味着编译器无法通过概念系统强制区分,因为它只能检查句法属性。

从介绍到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.

假设我们可以这样写:

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
  };

然而,这实际上并不会测试断言是否成立,因为 requires 子句不会在 运行 时被调用,而只会在编译时被检查。

regular_invocable 告诉函数的用户它将假定,调用具有相同参数值的 regular_invocable 函数的结果将产生相同的 return 值,并可能因此缓存该结果。

缓存结果可以由期望 regular_invocable 的函数完成,或者编译器可以使用该信息在参数值保持不变时优化​​对 regular_invocable 函数的多次函数调用相同。所以现在它可以被视为文档和编译器提示。

类似于const_cast,编译器可能并不总是能够检查它是否有效。由于这个原因,并且因为目前标准中没有 attribute/keyword 来将函数标记为始终 return 相同的值,所以现在没有办法在编译时强制函数传递 regular_invocable 确实符合该要求。