C++ 概念:“require”子句中的条件的执行到底有多严格?

wvn*_*wvn 8 c++ c++-concepts c++20

requires在这里,我详细介绍了概念中使用的子句的一个怪癖的 MWE 。我想要的是一个概念,指示某个函数类型是否可以通过一系列参数调用。我知道这是由 提供的std::invocable,但我在这里所提供的内容将说明这一点。

考虑以下概念:

template <typename func_t, typename... args_t>
concept callable = requires(const func_t& f, const args_t&... args) {f(args...);};
Run Code Online (Sandbox Code Playgroud)

这是相当简单的:如果我有一个func_t,我可以用 来调用它args_t...吗?根据我的理解,只要使用提供的参数调用函数是有效的操作(包括 conversions ) ,这个概念就应该评估为 true 。例如,如果我有一个 lambda:

auto func = [](const double& i) -> void {};
Run Code Online (Sandbox Code Playgroud)

那么以下两个概念的计算结果为true

callable<decltype(func), int>    //true
callable<decltype(func), double> //true
Run Code Online (Sandbox Code Playgroud)

这似乎是因为存在从int到 的转换double。这很好,因为这是我在项目中想要的行为,让我发现了这个问题。

现在,我想使用稍微复杂一点的类型来调用我的 lambda,如下所示:

auto func = [](const type1_t<space1>& t1) -> int {return 1;};
Run Code Online (Sandbox Code Playgroud)

考虑以下类型:

enum space {space1,space2};

template <const space sp> struct type2_t{};

template <const space sp> struct type1_t
{
    type1_t(){}

    template <const space sp_r>
    type1_t(const type2_t<sp_r>& t2){}
};
Run Code Online (Sandbox Code Playgroud)

这里我们可以转换type2_t为,type1_t而不管模板参数如何,因为type1_t. 在这些条件下,以下概念的计算结果为true

callable<decltype(func), type1_t<space1>> //true
callable<decltype(func), type2_t<space1>> //true
callable<decltype(func), type2_t<space2>> //true
Run Code Online (Sandbox Code Playgroud)

假设我不想在具有不同space参数的类型之间进行任何转换。有几种方法可以做到这一点,但我会选择在构造函数requires上使用子句type1_t

template <const space sp_r>
requires (sp_r == sp)
type1_t(const type2_t<sp_r>& t2)
{
    //all other code remains unchanged.
}
Run Code Online (Sandbox Code Playgroud)

经过这次机会,我得到以下评价:

callable<decltype(func), type1_t<space1>> //true
callable<decltype(func), type2_t<space1>> //true
callable<decltype(func), type2_t<space2>> //false
Run Code Online (Sandbox Code Playgroud)

这是我期望的行为,因为概念类中的代码requires不再编译。

现在,假设我删除了requires构造函数中的子句type1_t,并且构造函数现在调用一个名为 的成员函数dummy_func

template <const space sp> struct type1_t
{
    type1_t(){}

    template <const space sp_r>
    void dummy_func(const type2_t<sp_r>& t2){}

    template <const space sp_r>
    type1_t(const type2_t<sp_r>& t2)
    {
        dummy_func(t2);
    }
};
Run Code Online (Sandbox Code Playgroud)

构造函数几乎保持不变,因此所有概念都true再次计算为:

callable<decltype(func), type1_t<space1>> //true
callable<decltype(func), type2_t<space1>> //true
callable<decltype(func), type2_t<space2>> //true
Run Code Online (Sandbox Code Playgroud)

requires当我们引入一个子句时,就会出现奇怪的行为dummy_func

template <const space sp_r>
requires (sp_r == sp)
void dummy_func(const type2_t<sp_r>& t2){}
Run Code Online (Sandbox Code Playgroud)

通过该条款,我期望进行以下概念评估:

callable<decltype(func), type1_t<space1>> //true
callable<decltype(func), type2_t<space1>> //true
callable<decltype(func), type2_t<space2>> //false
Run Code Online (Sandbox Code Playgroud)

然而,当我使用 new 子句进行编译时,我实际上得到:

callable<decltype(func), type1_t<space1>> //true
callable<decltype(func), type2_t<space1>> //true
callable<decltype(func), type2_t<space2>> //true
Run Code Online (Sandbox Code Playgroud)

这对我来说很奇怪,因为以下内容将编译:

auto func = [](const type1_t<space1>& t1) -> int {return 1;};
func(type1_t<space1>());
Run Code Online (Sandbox Code Playgroud)

但这不会编译:

func(type2_t<space2>());
Run Code Online (Sandbox Code Playgroud)

callable<decltype(func), type2_t<space2>>对我来说,这与评估 to 的概念是矛盾的true,因为我直接使用子句中的代码主体requires

这种矛盾的根源是什么?requires为什么编译器没有完全检查概念子句中代码的有效性?

附录

两项免责声明:

  1. 我知道我应该使用std::invocable. 以上仅供说明之用。请注意,当我使用 时也会出现同样的问题std::invocable

  2. 我可以通过对 的构造函数施加约束来解决该问题type1_t,但这在我的项目中是不可取的。

有关显示该问题的完整代码,请参阅以下内容:

#include <iostream>
#include <concepts>

enum space
{
    space1,
    space2
};

template <typename func_t, typename... args_t>
concept callable = requires(const func_t& f, const args_t&... args) {f(args...);};

template <const space sp> struct type2_t{};

template <const space sp> struct type1_t
{
    type1_t(){}

    template <const space sp_r>
    requires (sp_r == sp)
    void dummy_func(const type2_t<sp_r>& t2){}

    template <const space sp_r>
    type1_t(const type2_t<sp_r>& t2)
    {
        dummy_func(t2);
    }
};

int main(int argc, char** argv)
{
    auto func = [](const type1_t<space1>& t1) -> int {return 1;};
    std::cout << callable<decltype(func), type1_t<space1>> << std::endl; //true
    std::cout << callable<decltype(func), type2_t<space1>> << std::endl; //true
    std::cout << callable<decltype(func), type2_t<space2>> << std::endl; //true, should be false!!
}
Run Code Online (Sandbox Code Playgroud)

请注意,我使用带有-std=c++20标志的 g++ 11.3。

Nic*_*las 11

对函数的约束约束了函数签名;它们与函数无关。约束并不关心主体是否无法编译;他们只关心应用于该函数签名的约束的有效性。因此,如果您希望对函数进行约束,则必须将这些约束放入该函数的签名中。

是的,这确实会递归地向上传播调用图。这只是你必须处理的事情。concept这就是为什么将任意表达式约束捆绑到命名中很有用的原因之一。