C++17在赋值表达式中定义了求值顺序:右手侧评估及其副作用被排序在左手侧评估及其副作用之前。但是,下面的代码显示,GCC中的“聚合赋值”违反了这一点:
#include <iostream>
#include <type_traits>
struct Trivial {
int x;
};
struct NonTrivial {
constexpr NonTrivial(int x = 0) : x{x} { }
int x;
};
constexpr int foo() {
Trivial a[1]{};
int i = 0;
a[i++] = {+i};
return a->x;
}
int postincrement(int& i) {
std::cout << "postincrement\n";
return i++;
}
int load(int& i) {
std::cout << "load\n";
return i;
}
int main() {
std::cout << "--aggregate assignment--\n";
std::cout << std::integral_constant<int, foo()>() << '\n';
std::cout << foo() << '\n';
{
Trivial a[1]{};
int i = 0;
a[i++] = Trivial{i * i};
std::cout << a->x << '\n';
}
std::cout << "\n--is not even indeterminately sequenced--\n";
{
Trivial a[1]{};
int i = 0;
(std::cout << "LHS\n", a[postincrement(i)])
= (std::cout << "RHS\n", Trivial{load(i)});
std::cout << a->x << '\n';
}
std::cout << "\n--copy assignment OK--\n";
// {
// NonTrivial a[1]{};
// int i = 0;
// a[i++] = NonTrivial{i};
// std::cout << a->x << '\n';
// }
{
NonTrivial a[1]{};
int i = 0;
a[i++] = NonTrivial{*&i};
std::cout << a->x << '\n';
}
std::cout << "\n--scalar assignment OK--\n";
// {
// Trivial a[1]{};
// int i = 0;
// a[i++].x = +i;
// std::cout << a->x << '\n';
// }
{
Trivial a[1]{};
int i = 0;
a[i++].x = +*&i;
std::cout << a->x << '\n';
}
}
字符串
第一个打印的值是0,正如预期的那样。第二个是0与O2,但1与O0。第三个值为1,第四个示例还显示右侧和左侧求值是交错的(输出RHS LHS postincrement load 1
),并且代码不会导致警告。第五个到第八个,不再使用“聚合赋值”,为0,注解掉的会导致警告,其他的不会。Clang将所有输出评估为0,并且不显示警告。据我从程序集判断,Msvc没有显示任何警告,并且只对第一个明显为常量求值的输出表示不同意。godbolt链接与这些和其他几个案件。
我见过几个类似的SO问题和GCC bug报告,但它们要么是多年前的问题,要么是在程序行为正确的情况下发出过多的警告。我试着检查C++17标准中的相关段落,但没有发现任何与赋值表达式求值顺序有关的“聚合赋值”异常。
在提交bug报告之前,我想确保我没有遗漏任何东西。GCC(和msvc)的行为真的不正确吗?
1条答案
按热度按时间v7pvogib1#
该标准仅规定,在对左操作数求值之前,对右操作数求值。
然而,在右手操作数
{i, 0}
或{i}
中,唯一相关的求值是i
,而没有任何左值到右值的转换,即在该评估中从不读取i
的值。i
的值只在以后调用operator=(X&&)
(X
是Trivial
或NonTrivial
)时读取,该赋值解析到operator=(X&&)
,以便从{i, 0}
构造临时对象来绑定X&&
参数。该标准仅对“operand”的求值进行了说明。我不清楚这是否包括临时构造(或为此应用于此类操作数的任何转换)。