constexpr 中允许未定义的行为——编译器错误?

Undefined behavior allowed in constexpr -- compiler bug?

我的理解是:

看来像下面这样的东西不应该编译,事实上在我的编译器上它不会。

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>>; 并首先将所有参数转换为 _Up50000 * 49999 = 2499950000 < 4294967296 = 2^32 不会导致溢出,无符号溢出在任何情况下都不会是 UB。

但是如果你有 gcdlcm 的模板代码,就像这样而不改变类型: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 消失。