c++ “requires”子句,包含从未满足的折叠表达式

j2cgzkjk  于 2023-06-25  发布在  其他
关注(0)|答案(1)|浏览(180)

我很难理解概念和约束是如何工作的。到目前为止,我总是设法避免使用类型traits和static_assertstd::enable_if(甚至SFINAE),但我想与c++20协调(至少在这一部分,因为我对几乎所有与c++20一起添加的东西都有同样的理解困难)。
我有一个带有可变参数模板参数的函数,我想只接受高于阈值的整数值,比如2
为此,我定义了一个integral概念,然后添加了一个requires子句来添加阈值约束,这给了我:

template <typename T>
concept integral = std::is_integral<T>::value;

template <integral ... Ts>
void f(Ts ... ts) requires (... && (ts > 2))
{
   //blablabla
}

这个编译得很好。但是当我尝试使用argumentsn调用f()(例如f(8, 6);)时,我总是得到编译时错误(GCC):error: 'ts#0' is not a constant expression
完整错误跟踪(GCC):

<source>: In substitution of 'template<class ... Ts>  requires (... && integral<Ts>) void f(Ts >...) requires (... && ts > 2) [with Ts = {int, int}]':
<source>:15:6:   required from here
<source>:8:6:   required by the constraints of 'template<class ... Ts>  requires (... && integral<Ts>) void f(Ts ...) requires (... && ts > 2)'
<source>:8:43: error: 'ts#0' is not a constant expression
   8 | void f(Ts ... ts) requires (... && (ts > 2))
     |                            ~~~~~~~~~~~~~~~^~
<source>: In function 'int main()':
<source>:15:6: error: no matching function for call to 'f(int, int)'
  15 |     f(8, 6);
     |     ~^~~~~~
<source>:8:6: note: candidate: 'template<class ... Ts>  requires (... && integral<Ts>) void f(Ts >...) requires (... && f::ts > 2)'
   8 | void f(Ts ... ts) requires (... && (ts > 2))
     |      ^
<source>:8:6: note:   substitution of deduced template arguments resulted in errors seen above

我不明白的是为什么参数必须是常量表达式,为什么8不被认为是常量表达式?

41zrol4v

41zrol4v1#

函数参数的值不能在函数约束中使用。重现问题的一个更简单的方法是:

#include <concepts>

// note: there already is a concept for integral types
//       also, we can use an abbreviated function template
void f(std::integral auto x) requires (x > 2)
{
    // ...
}

void foo() {
    f(0);
}

这会产生错误:

<source>:3:40: error: substitution into constraint expression
                      resulted in a non-constant expression
void f(std::integral auto x) requires (x > 2)
                                       ^~~~~
<source>:9:5: note: while checking constraint satisfaction
              for template 'f<int>' required here
    f(0);
    ^

x的值在编译时是未知的,函数约束只能验证编译时属性。即使我们使用x = 0调用ff也需要使用 * 所有 * 可能的参数,而不仅仅是0
如果你想要一个只能保存大于2的值的类型,你可以这样做:

template <std::integral T>
class greater_two_integer {
private:
    T v;
public:
    greater_two_integer(T x) : v{x} {
        assert(x > 2);
    }

    operator T() const noexcept {

// OPTIONAL: aid compiler optimizations
#if __has_cpp_attribute(assume)
        [[assume(v > 2)]];
#elif defined(__clang__)
        __builtin_assume(v > 2);
#elif __cpp_lib_unreachable == 202202L
        // from <utility>
        if (v <= 2) std::unreachable();
#endif

        return v;
    }
};
template <typename T>
void f(greater_two_integer<T> x) { /* ... */ }

void g(greater_two_integer<int> x) { /* ... */ }

int main() {
    f(greater_two_integer{10}); // OK
    g(10); // OK

    f(greater_two_integer{0}); // runtime check fails
    g(0); // runtime check fails
}

关于[[assume(v > 2)]]的说明

我们使用[[assume]](C++23起)来启用编译器优化,因为greater_two_integer总是包含一个值> 2。该属性应用于转换运算符中的空语句,因此当我们从对象中提取值时,如果v <= 2,则为未定义行为。
这是安全的,因为构造函数包含assert(v > 2),这意味着:

  • 我们总是先写一个值v > 2
  • 我们可以假设我们稍后将读取值v > 2
  • 从技术上讲 *,您可以通过将std::memcpy写入类中或通过reinterpret_cast<int*>写入其值来打破这个类不变量。然而,这两种方法显然都是搬起石头砸自己的脚,它们不是偶然发生的。

相关问题