Tob*_*ias 5 c++ constants compiler-optimization constexpr
在这段代码中,为什么c++编译器在编译时不直接返回1 test(),而是从内存中读取值?
struct Test {
const int x = 1;
// Do not allow initializing x with a different value
Test() {}
};
int test(const Test& t) {
return t.x;
}
Run Code Online (Sandbox Code Playgroud)
编译器输出:
test(Test const&): # @test(Test const&)
mov eax, dword ptr [rdi]
ret
Run Code Online (Sandbox Code Playgroud)
我本来期望:
test(): # @test(Test const&)
mov eax, 1
ret
Run Code Online (Sandbox Code Playgroud)
是否有任何符合标准的方法来修改 的值Test::x以包含与 不同的值1?或者是否允许编译器进行这种优化,但 gcc 和 clang 都没有实现它?
编辑:当然,您立即发现我将其作为最小示例的错误,即允许结构的聚合初始化。我使用空的默认构造函数更新了代码,以防止出现这种情况。(godbolt 上的旧代码)
我相信这是因为您仍然可以使用 x 的其他值和初始化列表来构造 Test 实例,如下所示:
Test x{2};
cout << test(x);
Run Code Online (Sandbox Code Playgroud)
演示: https: //www.ideone.com/7vlCmX
现在您已经禁止使用构造函数创建Test具有不同x值的对象实例,但 gcc/clang 仍然没有优化。
在不违反严格别名规则的情况下,使用char*或创建具有不同值的对象memcpy的对象表示可能是合法的。这将使优化非法。Testx
更新,见评论讨论;在 ISO 标准6.8.4 basic.type.qualifier中,“const 对象是类型的对象const T”,并且不排除它是一个子对象,通过指向结构的指针获取它可能只是算作一个const 对象的非常量访问路径。(任何在其生命周期内修改 const 对象的尝试都会导致未定义的行为,不会留下漏洞的空间,因为这是一个对象,而不是对对象的引用)。因此char*和memcpy方法看起来很 UB,甚至placement-new 也可能无济于事:Placement new 和使用 const 成员对类进行赋值- 仅当“原始对象的类型不是 const 限定的”时才允许重用。
const(关于不重用对象存储的语言在 C++20 中发生了变化;现在,new它为在整个非结构/类对象上使用placement- 敞开了大门const,即使它包含const 成员。)
Test即使在 ISO C++ 中,制造具有任意x值的全新对象std::bit_cast<Test>( int )似乎仍然是完全合法的。 它是可以简单复制的。此外,GCC 和 clang 等实际实现似乎定义了所有这些情况的行为,至少在事实上是这样;我没有检查他们的官方文档来看看它是否被称为 GNU 扩展。就优化器限制而言,这才是最重要的。
Test foo;
*(char*)&foo = 3; // change first byte of the object-representation
// which is where foo.x's value lives
Run Code Online (Sandbox Code Playgroud)
在 C++ 的引用上下文中,const意味着您不能通过此引用修改此对象。我不知道这如何适用于非对象的const 成员const。
这是标准布局类型,因此它应该与等效的 C 结构体二进制兼容,并且在没有 UB 的情况下也可以安全地往返write于read文件。它是一种 POD 类型(或者我认为 C++20 对 POD 概念的替代)。无论有没有Test(const Test&) = default;复制构造函数,它甚至都可以简单地复制,尽管这可能不相关。
如果将其写入文件并读回是合法的,那么即使文件在其间被修改,它仍然应该是明确定义的。或者,如果我们memcpy将其转换为数组,则修改该数组,然后复制回来:
Test foo;
char buf[sizeof(foo)];
memcpy(buf, &foo, sizeof(foo));
buf[0] = 3; // on a little-endian system like x86, this is buf.x = 3; - the upper bytes stay 0
memcpy(&foo, buf, sizeof(foo));
Run Code Online (Sandbox Code Playgroud)
唯一有问题的一步是最后的memcpy返回foo;这就是创建一个具有构造函数无法生成的值Test的对象的原因。x
@Klauss 提出了关于覆盖整个对象而不破坏它并对新对象进行新放置的担忧。我认为标准布局 POD 类型允许这样做,但我还没有检查标准。对于成员全部为非的结构或类,应该允许这样做const;这就是标准布局和POD / TrivialType的要点。无论如何,该char*版本都会避免这样做,而不是重写整个对象。
仅仅拥有一个成员是否会const破坏将对象表示写入/读取文件的能力?我不这么认为;拥有const成员并不会使类型失去标准布局、琐碎甚至琐碎可复制的资格。(这一点是最大的延伸;但我仍然认为这是合法的,除非有人可以在标准中向我展示在非常量类对象的对象表示中探索是不合法的。)
如果有或没有允许成员使用不同的初始化程序的构造函数const int x是 UB 或不将该对象写入/读取到文件并修改它之间的区别,那将是非常奇怪的。 就浏览对象表示的字节是否合法而言,无法以“正常”方式创建Test具有不同值的对象x是一个转移注意力的话题。(尽管这对于有成员的班级来说仍然是一个有效的问题const。)
@Tobias 还用一个示例(https://godbolt.org/z/3abaEqWdM)进行了评论,该示例使用 C++20std::bit_cast来制造一个constexpr 安全的Test对象x == 2,并且即使在static_assert. std::bit_cast
从这个例子中我们还可以看到,GCC 和 clang 为非内联函数调用留出了空间来修改已构造Test对象的该成员:
void ext(void*); // might do anything to the pointed-to memory
int test() {
Test foo; // construct with x=1
ext (&foo);
return foo.x; // with ext() commented out, mov eax, 1
}
Run Code Online (Sandbox Code Playgroud)
# GCC11.2 -O3. clang is basically equivalent.
test():
sub rsp, 24 # stack alignment + wasted 16 bytes
lea rdi, [rsp+12]
mov DWORD PTR [rsp+12], 1 # construct with x=1
call ext(void*)
mov eax, DWORD PTR [rsp+12] # reload from memory, not mov eax, 1
add rsp, 24
ret
Run Code Online (Sandbox Code Playgroud)
它可能是也可能不是错过的优化。许多错过的优化是编译器不会寻找的,因为它的计算成本很高(即使是提前编译器也不能在可能很大的函数上不小心使用指数时间算法)。
不过,这看起来并不太昂贵,只是检查构造函数默认值是否无法被覆盖。尽管在制作更快/更小的代码方面它的价值似乎很低,因为希望大多数代码不会这样做。
这无疑是一种次优的代码编写方式,因为您在保存该常量的类的每个实例中都浪费了空间。所以希望它不会经常出现在实际的代码库中。 static constexpr如果您有意拥有每个类常量,则这是惯用的,并且比每个实例的成员对象要好得多。const
然而,常量传播可能非常有价值,因此即使它很少发生,它也可以在发生的情况下进行重大优化。