constexpr 中允许未定义的行为——编译器错误?
Undefined behavior allowed in constexpr -- compiler bug?
我的理解是:
- C++ 中的有符号整数溢出是未定义的行为
- 常量表达式不允许包含未定义的行为。
看来像下面这样的东西不应该编译,事实上在我的编译器上它不会。
template<int n> struct S { };
template<int a, int b>
S<a * b> f()
{
return S<a * b>();
}
int main(int, char **)
{
f<50000, 49999>();
return 0;
}
但是,现在我改为尝试以下操作:
#include <numeric>
template<int n> struct S { };
template<int a, int b>
S<std::lcm(a, b)> g()
{
return S<std::lcm(a,b)>();
}
int main(int, char **)
{
g<50000, 49999>();
return 0;
}
g++、clang 和 MSVC 中的每一个都会愉快地编译它,尽管
The behavior is undefined if |m|, |n|, or the least common multiple of
|m| and |n| is not representable as a value of type
std::common_type_t<M, N>
.
(来源: https://en.cppreference.com/w/cpp/numeric/lcm)
这是所有三个编译器中的错误吗?或者如果 lcm 的行为不能代表结果,那么 cppreference 是否是错误的?
根据 [expr.const]/5,在常量评估期间不允许“具有 [intro] 到 [cpp] 中指定的未定义行为的操作”,但是:
If E satisfies the constraints of a core constant expression, but evaluation of E would evaluate an operation that has undefined behavior as specified in [library] through [thread], or an invocation of the va_start
macro ([cstdarg.syn]), it is unspecified whether E is a core constant expression.
我们通常将此概括为“语言UB必须在需要常量表达式的上下文中诊断,但库UB不一定需要诊断”。
这个规则的原因是,导致库 UB 的操作可能会或可能不会导致语言 UB,即使在不导致语言 UB 的情况下,编译器也很难一致地诊断库 UB。 (事实上 ,即使是某些形式的语言 UB 也不能被当前的实现一致地诊断出来。)
有些人也将语言 UB 称为“硬”UB,将库 UB 称为“软”UB,但我不喜欢这个术语,因为(在我看来)它鼓励用户认为“代码未指定是否出现 UB 语言”,因为它比“明确具有 UB 语言的代码”要好。但在这两种情况下,结果都是程序员无法编写执行此类代码的程序并期望 anything 正常工作。
问题是 std::lcm()
正在使用无符号进行计算
无论参数是什么类型。它在我的 STL 中使用 using _Up = make_unsigned_t<common_type_t<_Mn, _Nn>>;
并首先将所有参数转换为 _Up
。 50000 * 49999 = 2499950000 < 4294967296 = 2^32
不会导致溢出,无符号溢出在任何情况下都不会是 UB。
但是如果你有 gcd
和 lcm
的模板代码,就像这样而不改变类型:https://godbolt.org/z/zoxzsr45x
// GCD implementation
template<typename T, T m, T n>
constexpr T
gcd()
{
if constexpr (m == 0) {
return n;
} else if constexpr (n == 0) {
return m;
} else {
return gcd<T, n, T(m % n)>();
}
}
// LCM implementation
template<typename T, T m, T n>
constexpr T
lcm()
{
if constexpr (m != 0 && n != 0) {
return (m / gcd<T, m, n>()) * n;
} else {
return 0;
}
}
constinit auto t = lcm<int, 50000, 49999>();
int main(int, char **)
{
return 0;
}
然后编译器失败:
<source>: In instantiation of 'constexpr T lcm() [with T = int; T m = 50000; T n = 49999]':
<source>:27:42: required from here
<source>:21:37: warning: integer overflow in expression of type 'int' results in '-1795017296' [-Woverflow]
21 | return (m / gcd<T, m, n>()) * n;
| ~~~~~~~~~~~~~~~~~~~~~^~~
<source>:27:16: error: 'constinit' variable 't' does not have a constant initializer
27 | constinit auto t = lcm<int, 50000, 49999>();
| ^
<source>:27:42: in 'constexpr' expansion of 'lcm<int, 50000, 49999>()'
<source>:27:43: error: overflow in constant expression [-fpermissive]
27 | constinit auto t = lcm<int, 50000, 49999>();
| ^
在 Debian 下的 gcc-10 std::lcm
中定义为:
// std::abs is not constexpr, doesn't support unsigned integers,
// and std::abs(std::numeric_limits<T>::min()) is undefined.
template<typename _Up, typename _Tp>
constexpr _Up
__absu(_Tp __val)
{
static_assert(is_unsigned<_Up>::value, "result type must be unsigned");
static_assert(sizeof(_Up) >= sizeof(_Tp),
"result type must be at least as wide as the input type");
return __val < 0 ? -(_Up)__val : (_Up)__val;
}
/// Least common multiple
template<typename _Mn, typename _Nn>
constexpr common_type_t<_Mn, _Nn>
lcm(_Mn __m, _Nn __n) noexcept
{
static_assert(is_integral_v<_Mn>, "std::lcm arguments must be integers");
static_assert(is_integral_v<_Nn>, "std::lcm arguments must be integers");
static_assert(_Mn(2) == 2, "std::lcm arguments must not be bool");
static_assert(_Nn(2) == 2, "std::lcm arguments must not be bool");
using _Up = make_unsigned_t<common_type_t<_Mn, _Nn>>;
return __detail::__lcm(__detail::__absu<_Up>(__m),
__detail::__absu<_Up>(__n));
}
转换为 _Up
和 return 类型的 __absu
会导致 UB 消失。
我的理解是:
- C++ 中的有符号整数溢出是未定义的行为
- 常量表达式不允许包含未定义的行为。
看来像下面这样的东西不应该编译,事实上在我的编译器上它不会。
template<int n> struct S { };
template<int a, int b>
S<a * b> f()
{
return S<a * b>();
}
int main(int, char **)
{
f<50000, 49999>();
return 0;
}
但是,现在我改为尝试以下操作:
#include <numeric>
template<int n> struct S { };
template<int a, int b>
S<std::lcm(a, b)> g()
{
return S<std::lcm(a,b)>();
}
int main(int, char **)
{
g<50000, 49999>();
return 0;
}
g++、clang 和 MSVC 中的每一个都会愉快地编译它,尽管
The behavior is undefined if |m|, |n|, or the least common multiple of |m| and |n| is not representable as a value of type
std::common_type_t<M, N>
.
(来源: https://en.cppreference.com/w/cpp/numeric/lcm)
这是所有三个编译器中的错误吗?或者如果 lcm 的行为不能代表结果,那么 cppreference 是否是错误的?
根据 [expr.const]/5,在常量评估期间不允许“具有 [intro] 到 [cpp] 中指定的未定义行为的操作”,但是:
If E satisfies the constraints of a core constant expression, but evaluation of E would evaluate an operation that has undefined behavior as specified in [library] through [thread], or an invocation of the
va_start
macro ([cstdarg.syn]), it is unspecified whether E is a core constant expression.
我们通常将此概括为“语言UB必须在需要常量表达式的上下文中诊断,但库UB不一定需要诊断”。
这个规则的原因是,导致库 UB 的操作可能会或可能不会导致语言 UB,即使在不导致语言 UB 的情况下,编译器也很难一致地诊断库 UB。 (事实上 ,即使是某些形式的语言 UB 也不能被当前的实现一致地诊断出来。)
有些人也将语言 UB 称为“硬”UB,将库 UB 称为“软”UB,但我不喜欢这个术语,因为(在我看来)它鼓励用户认为“代码未指定是否出现 UB 语言”,因为它比“明确具有 UB 语言的代码”要好。但在这两种情况下,结果都是程序员无法编写执行此类代码的程序并期望 anything 正常工作。
问题是 std::lcm()
正在使用无符号进行计算
无论参数是什么类型。它在我的 STL 中使用 using _Up = make_unsigned_t<common_type_t<_Mn, _Nn>>;
并首先将所有参数转换为 _Up
。 50000 * 49999 = 2499950000 < 4294967296 = 2^32
不会导致溢出,无符号溢出在任何情况下都不会是 UB。
但是如果你有 gcd
和 lcm
的模板代码,就像这样而不改变类型:https://godbolt.org/z/zoxzsr45x
// GCD implementation
template<typename T, T m, T n>
constexpr T
gcd()
{
if constexpr (m == 0) {
return n;
} else if constexpr (n == 0) {
return m;
} else {
return gcd<T, n, T(m % n)>();
}
}
// LCM implementation
template<typename T, T m, T n>
constexpr T
lcm()
{
if constexpr (m != 0 && n != 0) {
return (m / gcd<T, m, n>()) * n;
} else {
return 0;
}
}
constinit auto t = lcm<int, 50000, 49999>();
int main(int, char **)
{
return 0;
}
然后编译器失败:
<source>: In instantiation of 'constexpr T lcm() [with T = int; T m = 50000; T n = 49999]':
<source>:27:42: required from here
<source>:21:37: warning: integer overflow in expression of type 'int' results in '-1795017296' [-Woverflow]
21 | return (m / gcd<T, m, n>()) * n;
| ~~~~~~~~~~~~~~~~~~~~~^~~
<source>:27:16: error: 'constinit' variable 't' does not have a constant initializer
27 | constinit auto t = lcm<int, 50000, 49999>();
| ^
<source>:27:42: in 'constexpr' expansion of 'lcm<int, 50000, 49999>()'
<source>:27:43: error: overflow in constant expression [-fpermissive]
27 | constinit auto t = lcm<int, 50000, 49999>();
| ^
在 Debian 下的 gcc-10 std::lcm
中定义为:
// std::abs is not constexpr, doesn't support unsigned integers,
// and std::abs(std::numeric_limits<T>::min()) is undefined.
template<typename _Up, typename _Tp>
constexpr _Up
__absu(_Tp __val)
{
static_assert(is_unsigned<_Up>::value, "result type must be unsigned");
static_assert(sizeof(_Up) >= sizeof(_Tp),
"result type must be at least as wide as the input type");
return __val < 0 ? -(_Up)__val : (_Up)__val;
}
/// Least common multiple
template<typename _Mn, typename _Nn>
constexpr common_type_t<_Mn, _Nn>
lcm(_Mn __m, _Nn __n) noexcept
{
static_assert(is_integral_v<_Mn>, "std::lcm arguments must be integers");
static_assert(is_integral_v<_Nn>, "std::lcm arguments must be integers");
static_assert(_Mn(2) == 2, "std::lcm arguments must not be bool");
static_assert(_Nn(2) == 2, "std::lcm arguments must not be bool");
using _Up = make_unsigned_t<common_type_t<_Mn, _Nn>>;
return __detail::__lcm(__detail::__absu<_Up>(__m),
__detail::__absu<_Up>(__n));
}
转换为 _Up
和 return 类型的 __absu
会导致 UB 消失。