如何检查我的概念是否足以实现我的函数?

Too*_*one 5 c++ c++-concepts c++20

当我编写模板函数时,我想使用概念,并且确信我在函数中没有使用超出概念指定的“契约”的东西。请参阅 C++ 核心指南T.10

激励性但人为的示例:

template <typename T>
concept maxable = std::totally_ordered<T>; // or require x < y -> bool

template <maxable T>
T max(T x0, T x1, T x2)
{
    T x = x0;
    if (x < x1)
        x = x1;
    if (x < x2)
        x = x2;
    return x;
}
Run Code Online (Sandbox Code Playgroud)

max(0, 1, 2) /*->2*/没问题,但是,哎呀,以下内容无法编译:

struct Val
{
    int x{};
    Val(int x) : x(x) {}
    Val(const Val&) = delete;
    auto operator<=>(const Val&) const = default;
};

auto v = max(Val{0}, Val{1}, Val{2});
Run Code Online (Sandbox Code Playgroud)

我的在线函数的实现'Val::Val(const Val &)': attempting to reference a deleted function中出现错误。T x = x0;

我可以通过添加std::copy_constructible<T>编译错误(在 MSVC 上)来修复我的概念中的错误

error C2672: 'max': no matching overloaded function found
could be 'T max(T,T,T)'
note: the associated constraints are not satisfied
note: the concept 'maxable<Val>' evaluated to false
note: the concept 'std::copy_constructible<Val>' evaluated to false
Run Code Online (Sandbox Code Playgroud)

这就是我想要的——用户会收到有关未满足约束的警告,而不必查看我的实现。但是有没有一些方便的方法来检查我是否忘记了我的概念中的某些内容,或者相反,使用了我不应该使用的内容?我假设编写标准库算法的专家必须有某种方法来解决这个问题——或者我认为这一切都是错误的?

Jan*_*tke 5

简短的回答是这是不可能的。C++ 没有所谓的约束泛型,其中编译器确保除了约束保证的内容之外,没有什么可以在函数模板中使用。

最常见的问题之一是忘记初始化和赋值的约束。您忘记考虑复制赋值运算符,该运算符仅由std::copyable.

以下是正确的做法:

template <typename T>
concept maxable = std::totally_ordered<T> && std::copyable<T>;

template <maxable T>
T max(T x0, T x1, T x2)
{
    T x = x0;   // call to non-explicit copy constructor (std::copyable)
    if (x < x1) // comparison (std::totally_ordered)
        x = x1; // copy assignment operator (std::copyable)
    if (x < x2) // comparison (std::totally_ordered)
        x = x2; // copy assignment operator (std::copyable)
    return x;   // non-explicit move constructor or copy elision (std::copyable)
}               // OK, function is sufficiently constrained :)
Run Code Online (Sandbox Code Playgroud)

然而,max可以说是过度约束,因为我们不需要可复制的类型,并且我们不需要 的赋值运算符max。通过不同的实现,可以有更少的限制。

template <typename T>
concept maxable = std::totally_ordered<T> && std::move_constructible<T>;

template <maxable T>
T max(T x0, T x1, T x2)
{
    return x0 < x1 ? x1 < x2 ? std::move(x2) : std::move(x1)
                   : x0 < x2 ? std::move(x2) : std::move(x0);
}
Run Code Online (Sandbox Code Playgroud)

如果您像这样做一样选择了参考std::max,您甚至可以避免std::move_constructible.

结论

有两个教训值得吸取:

  1. 不受到约束是极其困难的。像没有赋值运算符的复制构造类型这样的陷阱很容易被忘记。请注意,std::copy_constructible<T>还需要std::convertible_to<T, T>考虑explicit构造函数。当设计自己的概念时,你也必须记住这些细节。

  2. 不过度约束是极其困难的。通常存在一种更简单的实现,不需要可复制性,或者不需要赋值运算符。这是有问题的,因为更改约束会影响重载决策中函数的顺序,从而破坏 API。


注意:因为复制初始化需要非显式构造函数,并且因为列表初始化可能使用std::initializer_list构造函数(顺便说一下,它在重载解析中胜过复制构造函数),因此在有疑问时应该更喜欢直接初始化。std::copyable涵盖了复制初始化,但在函数模板中使用直接初始化仍然是一个好习惯。