为什么使用成员数组调用constexpr函数不是常量表达式?

28

我有以下辅助函数:

template<typename T, std::size_t N>
constexpr std::size_t Length(const T(&)[N]) {
    return N;
}

返回静态数组的长度。过去这总是有效的,但当我执行以下操作时:

struct Foo
{
    unsigned int temp1[3];
    void Bar()
    {
        constexpr std::size_t t = Length(temp1); // Error here
    }
};

我在使用MSVS 2017时遇到了错误:
error C2131: expression did not evaluate to a constant

note: failure was caused by a read of a variable outside its lifetime

note: see usage of 'this'
我希望有人能够解释一下我做错了什么。

虽然无关紧要,但你最好也将 Length 标记为 noexcept - 没有任何合理的情况会导致它抛出异常。 - Jesper Juhl
1
gcc上似乎工作正常。 - super
1
在我看来,这似乎是MSVC的一个bug。这应该是一个常量表达式,我不明白为什么它不是。 - SergeyA
2
@super:不,它在GCC中不起作用:http://coliru.stacked-crooked.com/a/927ce1d01daf819e。GCC没有`-pedantic-errors`可能会允许它,因为它支持C++中的VLA。但是,在C++17 GCC中使用-pedantic-errors也会失败。 - AnT stands with Russia
1
@JesperJuhl 对不起,版本14.14.26428。 - James Nguyen
显示剩余5条评论
3个回答

21

MSVC是正确的。 Length(temp1)不是一个常量表达式。 参见[expr.const]p2

表达式e是核心常量表达式,除非根据抽象机器的规则,e的评估将评估以下表达式之一:

  • this,除非在constexpr函数或作为e的一部分正在评估的constexpr构造函数中;

temp1隐式地评估了this(因为您正在引用this->temp1),因此您没有常量表达式。 gcc和clang接受它,因为它们支持作为扩展的VLA(尝试使用-Werror = vla-pedantic-errors编译)。

为什么不允许这样做? 好吧,您可以访问底层元素并可能修改它们。 如果您处理的是constexpr数组或正在评估为常量表达式的数组,则完全可以,但如果不是,则不可能拥有常量表达式,因为您将操作设置为运行时的值。


4
如果您想使用GCC测试代码的标准有效性,那么在其众多扩展之间小心翼翼地使用狭窄针对性的“-Werror=vla”选项并没有太多意义。在这种情况下,正确的选项组合是“-std=...”和“-pedantic-errors”。 - AnT stands with Russia
5
我完全同意禁用扩展通常是应该采取的方式,但我们在这里特别讨论VLA,所以我想提一下-Wvla警告选项。此外,有时候为了实际操作需要,消除代码库中的扩展必须逐个进行。 - Jesper Juhl
你的回答听起来有点绕... 我认为直接回答这个问题的方式应该是 "编译器没有考虑到你忽略实际元素值的事实,这些值不是常量表达式"。 - user541686
@Jesper Juhl:是的,我总体上同意。我只是想说,在这种特定情况下,如果我在GCC中没有使用-pedantic-errors编译,我甚至不会收到警告。完全没有任何提示。也就是说,我没有理由怀疑有什么问题,更不用担心像VLA这样具体的问题。 - AnT stands with Russia
1
@Mehrdad 这听起来像是一个实现质量问题。但是,如果编译器在不发出任何诊断消息的情况下接受了这段代码,那么从技术上讲,它是不正确的。 - aschepler
@aschepler:嗯...我不是那么看,但好吧。 - user541686

4
Length(decltype(temp1){})

看起来可行

不幸的是,我无法发表评论,但是Mehrdad的解决方案是错误的。原因:虽然它不是技术上未定义行为,但实际上它确实是未定义行为。在constexpr求值期间,编译器必须捕获未定义行为。因此,代码存在问题


1
代码格式有误,正如我上面的链接所示:gcc 拒绝它。因此,没有扩展名。 - Mi-He

1
您的问题已经得到解答,但是在如何“修复”它方面,一个快速而简单的方法是将其替换。
Length(temp1)

使用

Length(*(true ? NULL : &temp1))

我认为这在技术上是未定义的行为,但在实践中对于MSVC来说应该可以正常工作。
如果您需要一个能够在UB情况下正常工作的解决方案,您可以将Length更改为使用指针:
template<typename T, std::size_t N>
constexpr std::size_t Length(const T(*)[N]) {
    return N;
}

然后您可以使用Length(true ? NULL : &temp1)


它更像是一个扩展而不是UB吗?因为在常量表达式中的UB会导致不合法,但实现可以通过扩展来规避这个问题。 - Rakete1111
@Rakete1111:这似乎是有道理的,但我真的不知道。我只是试图提供一个解决方法,以防有人需要让代码工作(我已经在MSVC上测试过了)。 - user541686

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接