是否可以根据 C++11 表达式是否是 C++11 中的常量表达式(即constexpr
)来生成编译时布尔值?关于 SO 的一些问题与此有关,但我在任何地方都没有看到直接的答案。
5 回答
我曾经写过它(编辑:见下文的限制和解释)。来自https://stackoverflow.com/a/10287598/34509:
template<typename T>
constexpr typename remove_reference<T>::type makeprval(T && t) {
return t;
}
#define isprvalconstexpr(e) noexcept(makeprval(e))
然而,常量表达式有很多种。上面的答案检测纯右值常量表达式。
解释
noexcept(e)
表达式给出当false
且仅当e
包含
- 对没有非抛出异常规范的函数的潜在评估调用,除非调用是常量表达式,
- 一个可能被评估的
throw
表达式, dynamic_cast
or的一个潜在评估的可抛出形式typeid
。
注意函数模板makeprval
没有声明noexcept
,所以调用需要是一个常量表达式,第一个子弹不适用,这是我们滥用的。我们还需要其他项目符号不适用,但谢天谢地,athrow
和 throwable dynamic_cast
ortypeid
也不允许在常量表达式中使用,所以这很好。
限制
不幸的是,有一个微妙的限制,这对您来说可能很重要,也可能无关紧要。“潜在评估”的概念比常量表达式应用的限制要保守得多。所以上面noexcept
可能会给出假阴性。它将报告某些表达式不是纯右值常量表达式,即使它们是。例子:
constexpr int a = (0 ? throw "fooled!" : 42);
constexpr bool atest = isprvalconstexpr((0 ? throw "fooled!" : 42));
在上面atest
是假的,即使初始化a
成功了。这是因为作为一个常量表达式,“邪恶的”非常量子表达式“从不评估”就足够了,即使这些邪恶的子表达式在形式上是潜在的评估。
截至 2017 年,is_constexpr
在 C++11 中是不可能的。这听起来很奇怪,所以让我解释一下历史。
首先,我们添加了这个特性来解决一个缺陷:http ://www.open-std.org/jtc1/sc22/wg21/docs/cwg_defects.html#1129
Johannes Schaub - litb 发布了一个 constexpr 检测宏,该宏依赖于常量表达式隐式为 noexcept 的规定。这在 C++11 中有效,但至少有一些编译器(例如 clang)从未实现过。然后,作为 C++17 的一部分,我们评估了Removing Deprecated Exception Specifications from C++17。作为该措辞的副作用,我们不小心删除了该条款。当核心工作组讨论重新添加该条款时,他们意识到这样做存在一些严重问题。您可以在LLVM 错误报告中查看完整的详细信息。因此,我们决定将其视为对所有标准版本的缺陷,而不是重新添加,并追溯删除它。
这样做的效果是,据我所知,没有办法检测表达式是否可用作常量表达式。
是的,这是可能的。一种方法(即使最近的noexcept
更改也有效)是利用 C++11 缩小转换规则:
窄化转换是从整数类型或无作用域枚举类型到整数类型的隐式转换 [...],它不能表示原始类型的所有值,除非源是一个常量表达式,其整数提升后的值将适合进入目标类型。
(强调我的)。列表初始化通常不允许缩小转换,当与 SFINAE 结合使用时,我们可以构建小工具来检测任意表达式是否为常量表达式:
// p() here could be anything
template<int (*p)()> std::true_type is_constexpr_impl(decltype(int{(p(), 0U)}));
template<int (*p)()> std::false_type is_constexpr_impl(...);
template<int (*p)()> using is_constexpr = decltype(is_constexpr_impl<p>(0));
constexpr int f() { return 0; }
int g() { return 0; }
static_assert(is_constexpr<f>());
static_assert(!is_constexpr<g>());
现场演示。
这里的关键是int{(expr, 0U)}
包含从unsigned int
to的缩小转换int
(因此格式不正确),除非 expr
是一个常量表达式,在这种情况下,整个表达式(expr, 0U)
是一个常量表达式,其评估值适合 type int
。
以下是用于 C++11 和 C++17的is_constexpr
for functions的实现,而不是任意表达式。但是,它要求您要测试的函数的参数是默认可构造的。
#include <type_traits>
struct A {}; // don't make it too easy, use a UDT
A f1(A a) { return a; } // is_constexpr -> false
constexpr A f2(A a) { return a; } // is_constexpr -> true
// The following turns anything (in our case a value of A) into an int.
// This is necessary because non-type template arguments must be integral
// (likely to change with C++20).
template <class T> constexpr int make_int(T &&) { return 0; }
// Helper to turn some function type (e.g. int(float)) into a function
// pointer type (e.g. int (*)(float)).
template <class T> struct signature_from;
template <class R, class... Args> struct signature_from<R(Args...)> {
using type = R(*)(Args...);
};
// See std::void_t for the idea. This does it for ints instead of types.
template <int...> using void_from_int = void;
// The fallback case: F is not a function pointer to a constexpr function
template <class T, typename signature_from<T>::type F, class = void_from_int<>>
struct is_constexpr {
static constexpr bool value = false;
};
// If void_from_int<make_int(F(Args()...))> doesn't lead to a substitution
// failure, then this is the preferred specialization. In that case F must
// be a function pointer to a constexpr function. If it is not, it could
// not be used in a template argument.
template <class R, class... Args, typename signature_from<R(Args...)>::type F>
struct is_constexpr<R(Args...), F, void_from_int<make_int(F(Args()...))>>
{
static constexpr bool value = true;
};
// proof that it works:
static_assert(!is_constexpr<A(A), f1>::value, "");
static_assert( is_constexpr<A(A), f2>::value, "");
#if __cplusplus >= 201703
// with C++17 the type of the function can be deduced:
template<auto F> struct is_constexpr2 : is_constexpr<std::remove_pointer_t<decltype(F)>, F> {};
static_assert(!is_constexpr2<f1>::value, "");
static_assert( is_constexpr2<f2>::value, "");
#endif
在https://godbolt.org/g/rdeQme上查看它的实际应用。
添加了 C++20std::is_constant_evaluated()
这允许检查某个表达式是否为常数计算表达式,即在编译时被计算。
使用示例:
constexpr int foo(int num) {
// below is true in case the condition is being evaluated at compile time
// side note, using: if constexpr (std::is_constant_evaluated())
// would be evaluated always to true, so you should use a simple if!
if (std::is_constant_evaluated()) {
return foo_compiletime(num);
}
else {
return foo_runtime(num);
}
}
int main() {
constexpr auto t1 = foo(6); // reaches foo_compiletime
const auto t2 = foo(6); // reaches foo_compiletime
int n = rand() % 10;
const auto t3 = foo(n); // reaches foo_runtime
auto t4 = foo(6); // unfortunately, reaches foo_runtime
}
上面示例中的最后一次调用将到达foo_runtime,因为该调用不在常量表达式上下文中(结果未用作常量表达式,另请参见此SO answer)。
与将决定留给用户的情况相比,这可能会导致不希望的悲观,用户可能会调用:
auto t4 = foo_compiletime(6);
并且编译器可以在编译时执行foo_compiletime内的操作,如果它被声明为constexpr
函数,或者如果它被声明则有义务这样做consteval
。但是,一旦我们将决定留给编译器,我们将到达foo_runtime,除非我们通过将结果放入 a或变量中明确指示编译器执行foo_compiletime 。然后,如果需要用户帮助编译器查看正确的路径,那么在某种程度上,这忽略了在两种情况下都有一个函数的价值。const
constexpr
constinit
要优化调用的另一个可能选项是:
constexpr auto temp = foo(6); // foo_compiletime
auto t4 = temp;
但同样,我们要求用户了解foo的内部行为,这并不是我们想要实现的。
请参阅此代码中的悲观化。
在这篇关于该主题的精彩博客文章中查看更多信息。