前行提要
1.C++17 及以后版本中引入的一个重要的语义变化,它与内存模型和对象生命周期有关。
之前的 C++ 标准中,只要一个指针指向了合法的内存地址,并且类型正确,那么通过该指针访问对象通常都是定义良好的。但是在 C++17 中,这种假设被放松了。
2.(借鉴)先提两个在c++中的术语:
IB(Implementation-defined Behaviour):是实现定义的行为-编译器必须记录其行为。例如,对普通算术执行运算。
UB(undefined behavior):未定义的行为-编译器可以执行任何操作,包括简单地崩溃或给出不可预测的结果。取消引用空指针属于该类别,但还包括诸如指针算术之类的更巧妙的事情,它们不在数组对象的范围之内。
在c++中,经常会有对对象中对象的获取,或者反其道而行之的具体场景。这个在Linux内核中屡见不鲜。c/c++中一个非常灵活的方式,是对指针的强制转换和各种前进后退指针值,然后而,具体到内存中,这段对象到底是什么类型,可不可以转换成某种类型,这都是一个无法确保的事情(UB)。那么,结果就是有可能Crash或者其它意外的情况。其它一些高级语言为了解决类似的问题,干脆直接禁掉了指针。而c/c++呢?也禁掉?这显然是不现实的。那么,不断的通过各种手段将其安全化,这才是正道。
这里有一个问题需要澄清一下,未定义行为不代表一定会崩溃,不同的编译器可能给出来的结果相同或者说不同都是可能的,一定要明白未定义行为的含义。否则就会产生矛盾的想法。
具体来说:
std::launder
是 C++17 标准中引入的一个函数,用于处理C++中的"对象重新表示(object representation)"问题。让我们来详细了解一下它的作用和使用方法。
-
原因:
- C++17 引入了更严格的内存模型规则,以更好地反映现代硬件和编译器优化的实际行为。
- 原来被认为合法的一些指针操作,实际上可能会干扰编译器的优化,导致程序行为不可预测。
-
新规则 铁律 死记
- 即使一个指针指向了合法的内存地址,并且类型正确,但如果这个指针所指向的对象不再"活着",那么通过它访问对象就是未定义行为。
- "活着":对象必须在其生命周期内,并且没有被销毁或重新表示。
- 重新表示:将一种类型的对象以另一种类型的方式访问或修改。这种行为的语义在 C++ 标准中有明确的定义。涉及的关键字:对象生命周期,可访问性,严格别名(即不同类型但共享内存的对象),常量性,未定义行为。这些一旦变动(底层二进制形式),说明重新表示即触发了
- "活着":对象必须在其生命周期内,并且没有被销毁或重新表示。
- 即使一个指针指向了合法的内存地址,并且类型正确,但如果这个指针所指向的对象不再"活着",那么通过它访问对象就是未定义行为。
-
为什么需要
std::launder
?- 当对象重新表示发生变化时,编译器可能会作出一些假设并进行优化,这可能导致一些边缘情况下的错误。
std::launder
的作用就是告诉编译器:"我已经改变了对象的表示,请放弃之前的假设并重新优化。"- 之前假设??重新优化??我不造啊
- 解释:这里涉及到编译器的内存模型假设和优化机制。关键字:
- 编译器的内存模型假设
- 编译器在编译代码时,会根据源代码构建一个内存模型,即对象在内存中的布局和生命周期。编译器会基于这个内存模型进行各种优化,比如消除冗余的内存访问,执行内联等。
- 对象的重新表示
- 当程序通过
reinterpret_cast
或其他方式重新表示对象时,可能会破坏编译器原有的内存模型假设。 - 比如,程序可能会创建一个新的对象,并将其放在与原有对象相同的内存位置。
- 当程序通过
- 编译器的假设被破坏
- 一旦对象的表示被改变,编译器原有的内存模型假设就会被破坏。
- 编译器可能会基于错误的假设进行优化,从而导致程序出现未定义行为。
- 编译器的内存模型假设
-
std::launder
的作用:std::launder
告诉编译器:"我已经改变了对象的表示,请放弃之前的假设并重新优化。"- 这样,编译器就可以放弃原有的内存模型假设,并根据新的对象表示进行优化。
换句话说,std::launder
是一种向编译器"坦白"的方式,让编译器知道对象的表示已经被改变,从而避免它继续使用无效的假设进行优化。这样做可以确保程序的正确性和性能。
举例
#include <new>
#include <cstddef>
#include <cassert>
#include<iostream>
#include<stdio.h>
struct A {
virtual int transmogrify();
};
struct B : A {
int transmogrify() override { new(this) A; return 2; }
};
//第一次运行时 在a的对象中构建b
int A::transmogrify() { new(this) B; return 1; }
static_assert(sizeof(B) == sizeof(A));
int main()
{
// 测试 :逆反内存模型规则
A i;
int n = i.transmogrify(); //这里调用的是 A::transmogrify() 函数,它创建了一个新的 B 对象,该对象是 A 对象的子对象
// int m = i.transmogrify(); // 未定义行为 前一次调用已经在 i 对象上创建了一个 B 子对象 内存模型发生了改变,导致这是未定义行为。
int m = std::launder(&i)->transmogrify(); // OK 通过launder 告诉编译器 给我套新房(内存空间) 不再使用以前的内存模型。 因为它会触发重新表示,导致不是“活着”,最终引发编译器发出未定义
assert(m + n == 3);
return 0;
}
总结一下
在c++17中:
“According to this answer, since C++17, even if a pointer has the right address and the right type dereferencing it can cause undefined behaviour.”
以前的 C++ 标准中,只要一个指针指向了合法的内存地址,并且类型正确,那么通过该指针访问对象通常都是定义良好的。但是在 C++17 中,这种假设被放松了。
cpp新标准 引入内存操作,随之携带了很多关于内存方面的工具。launder即是与编译器沟通,告知编译器,一个指针所指向的对象已经被重新表示或修改,编译器应该放弃之前的假设。
最后随着cpp新特性学习,最终得出的结论是cpp越新,越能体会到,让编译器来写重复的代码,这就导致我们需要跟编译器沟通,在编译时告知它坑点,“这不应该这样做 应该这样做”。