clang 和 gcc 中的 Constexpr 复合赋值运算符

Constexpr compound assignment operator in clang and gcc

我有以下代码:

main.cpp

#include <cstdint>
#include <type_traits>

enum class FooEnum : uint8_t{
    Foo1 = 0, 
    Foo2 = 1
};

constexpr uint32_t& operator|= (uint32_t& lhs, FooEnum rhs) {
    return lhs |= 1u << static_cast<uint8_t>(rhs);
}

int main() {
    uint32_t bar{0};
    bar|=FooEnum::Foo1;
}

所以本质上,|= 运算符应该采用枚举并设置位,其位置对应于它的整数值。

在 fedora 21 上用 clang++ 3.5.0 编译时,一切正常,但用 g++ 4.9.2 编译时,它抛出一个 error 说这不是 常量表达式:

main.cpp: In function ‘constexpr uint32_t& operator|=(uint32_t&, FooEnum)’:
main.cpp:16:2: error: expression ‘(lhs = (lhs | (1u << ((int)rhs))))’ is not a constant-expression
  }
  ^

这适用于所有类型的编译器标志组合,但您可以例如用 g++ -std=c++11 -o a.out main.cpp 测试它(c++14 没有区别)

所以我的问题是:

  1. 哪个编译器是正确的(为什么)?
  2. 有没有办法实现 operator|= 以便 g++ 将其作为 constexpr 接受?

编辑:
如果您想知道,为什么我首先尝试将运算符声明为 constexpr,尽管示例中不需要这样做:
在我的实际代码中,我使用 |=-operator 来实现 (constexpr) |-operator,我想在 constexpr 表达式中使用它,但在此之前,我迷迷糊糊了关于两个编译器之间的差异,没有意识到 gcc4.9 不完全支持 c++14(但接受 -std=c++14 标志)。
当使用运算符实际初始化一个全局 constexpr 变量时,即使是 clang 也只用 c++14 标志编译它。

表达式lhs |= 1u << static_cast<uint8_t>(rhs)永远不能是常量表达式本身,因为它修改了lhs。在 C++14 中禁止这样做的规则是 §5.19/2.15(C++11 中也存在一个有效等效的规则):

A conditional-expression e is a core constant expression unless the evaluation of e, following the rules of the abstract machine (1.9), would evaluate one of the following expressions:

  • modification of an object (5.17, 5.2.6, 5.3.2) unless it is applied to a non-volatile lvalue of literal type that refers to a non-volatile object whose lifetime began within the evaluation of e;

在 C++11 中,由于 §7.1.5/5,它必须是一个:

For a constexpr function, if no function argument values exist such that the function invocation substitution would produce a constant expression (5.19), the program is ill-formed; no diagnostic required.

不存在使 returned 表达式在调用替换后成为常量表达式的参数:赋值阻止了这种情况。因此该程序在 C++11 中格式错误(但不需要诊断),并且在使用 -std=c++11 编译时,GCC 显示兼容行为。
在 C++14 中,该规则被调整:

For a non-template, non-defaulted constexpr function […], if no argument values exist such that an invocation of the function […] could be an evaluated subexpression of a core constant expression (5.19), the program is ill-formed; no diagnostic required.

这使得 return 表达式本身成为一个非常量表达式,只要该函数在另一个核心常量表达式中是可计算的,例如来自另一个 constexpr 函数:

constexpr auto foo(FooEnum rhs)
{
    uint32_t x = 0;
    x |= rhs;
    return x;
}

foo(FooEnum::Foo1)是一个核心常量表达式,因此operator|=可以在核心常量表达式中调用,因此函数定义是合式的。

正如@dyp 在评论中指出的那样,GCC 自第 5 版以来仅支持 "relaxing constraints on constexpr functions"-功能。GCC 5.1 compiles your code

所以现在 constexpr 函数的主体通常由本身不是常量表达式的语句组成。第一个引用部分后面的示例显示了一个函数 incr,GCC 也会拒绝该函数:

constexpr int incr(int &n) {
    return ++n;
}

constexpr int h(int k) {
    int x = incr(k); // OK: incr(k) is not required to be a core
                     // constant expression
    return x;
}