一、std::launder的回顾
template <typename T> class coreoptional { private: T payload; public: coreoptional(const T& t) : payload(t) { } template<typename... Args> void emplace(Args&&... args) { payload.~T(); ::new (&payload) T(std::forward<Args>(args)...); } const T& operator * () const & { return payload; } }; //If here T is a structure with constant or reference members: struct X { const int _i; X(int i) : _i(i) {} friend std::ostream& operator<< (std::ostream& os, const X& x) { return os << x._i; } }; then the following code results into undefined behavior: coreoptional<X> optStr{42}; optStr.emplace(77); std::cout << * optStr; // undefined behavior (probably outputs 42 or 77)
讯享网
这段代码很简单,主要关注点在emplace,它表明调用这个函数会在payload原地创建一个新T对象。而这里恰恰有问题,那就是当一个T 对象调用此函数后,其本身做为一个临时对象已经被释放,而此时如果调用返回其实例的重载运算符就可能会产生UB的事件。当然,这里有一个前提就是这个对象必须是const修饰或者如果其为类类型其部有const修饰时。这个很好理解,毕竟内存变了,但const又不能允许变,那么到底应该返回什么 呢?这就是语法上的冲突了。
所以要解决上面的问题,就需要增加一个辅助的变量T*ptr_,来拿到 placement new后的新的地址和新的值。请注意,不要认为一个地址生成的两个对象是一个东西 。举一个不恰当的例子,你的房子过户给了别人,地址还是那个地址,但还是你的么?
所以这个新的辅助变量看上去是必须的,可从情理上讲,同一个地址你反复搞两回,是不是有点过分?所以只要使用std::launder在原地把对象返回去即可即:
讯享网return std::launder<T>(&payload);
其实也可以在emplace时使用,那么此处就可以不再使用了。
这其实就是在对glvalue使用时,标准要求对象必须已经存在,而上面的payload在调用函数emplace时已经销毁,这里只不过原地重建罢了。这和古建筑原地重建一样,即使完全的一模一样,人们也不会认为二者是同一个东西。所以C++标准要求placement new 后必须使用新的指针来返回对象。
那么标准为什么会这么要求呢?一个重要的原因就是,为了优化,看下面的例子:
struct A { const int i; }; void foo(A* p); int main() { A a{ 42 }; foo(&a); return a.i; }
讯享网struct X { const int n; }; union U { X x; float f; }; void tong() { U u = {
{ 1 }}; u.f = 5.f; // OK, creates new subobject of u X * p = new (&u.x) X {2}; // OK, creates new subobject of u assert(p->n == 2); // OK ... assert(u.x.n == 2); // undefined behavior, u.x does not name new subobject }
另外,在一些支持硬件 pointer provenance 的架构的机器上,std::launder可以得到一些相关的生命周期的信息(provenance主要还是为了内在安全和相关信息),这有点超纲了。有兴趣可以自己去看看。
二、std::start_lifetime_as
在分析start_lifetime_as之间,先看一个小例子来引入:
struct X { int a, b; }; X* make_x() { X* p = (X*)malloc(sizeof(struct X)); // implicitly creates an object of type X p->a = 1; p->b = 2; return p; }
这段代码是不是看起来非常平常,估计很多人都写过类似代码,甚至在教科书上都看到过类似的代码。这里面有什么问题么?没问题,至少在不少的平台跑起来非常好。但和标准对比一下,就会发现有问题。
C++20前标准中只有程序给对象分配好了存储空间并且初始化完成了,一个对象的生命周期才算开始,而一个对象的生命周期开始而未结束才被认为对象的存在。那么上面的代码照着语法分析,就会发现不和谐了。怎么办?Fix啊,打补丁。先是声明类似malloc这些标准库以及编译器厂商的类似函数可以 实现隐式的对象创建(这纯粹就是垄断),但面对大量的自定义的非标准的库,标准委员会也不得不作出妥协,这就提出了std::start_lifetime_as。
先扯点远的,不一定非得搞一个新的东西出来啊?完全可以用前面的placement new啊?但是这里有一个问题,无论哪种内存分配,一般在分配完成后,内存中的原有数据就是没用的了。可有些情况下,比如从文件读出一些序列化数据流,从Socket中接到的一大批序列化数据流等等,都需要重建数据,这里如果原地搞一下new,估计接收者就疯了。
但是以往的经验又告诉开发者,其实这种在c/c++的直接序列化中(未使用第三方的序列化库)直接强转(或static_cast)更简单更容易理解。至少在以前看到的很多IO通信中的案例中,都是如此操作的。这就替代了std::launder和std::start_lifetime_as的部分应用。仁者见仁吧,总之标准是标准,实际情况又是实际情况。反正编译器厂商也不是标准的应声虫。
所以看一下上面的代码如何修正:
讯享网struct X { int a, b; }; X * make_x() { X * p = std::start_lifetime_as<X>(myMalloc(sizeof(struct X)); p->a = 1; p->b = 2; return p; }
template< class To, class From > constexpr To bit_cast( const From& from ) noexcept;
三、二者的关系
更多的内容可以看一下相关的文档:
https://www.open-std.org/jtc1/sc22/wg21/docs/papers/2017/p0532r0.pdf
https://www.open-std.org/jtc1/sc22/wg21/docs/papers/2022/p2590r2.pdf
四、总结
学习新知识,回顾老知识和老问题,这次真得是弄明白了很多东西。原来只是觉得可以,现在知道是为什么。
越往C++的底层学习,就会发现大量东西已经无法和标准断开了学习了。在上层应用,只要会它的定义适用范围,这基本就好了。但在底层,一些语法标准定义出来后,在实际应用中,会产生各种想不到的情况,就需要对标准完善和升级,这都是一个缓慢但又必须要做的事情。
而从这些标准看起来,就会发现,其它语言和C++之间的千丝万缕的联系,对于学习其它语言有着更容易理解和直达原理以及产生的原因。这也是学习较为底层的语言一个优势或者说长处。
生命周期是各个语言的一个重点的设计模块,它涉及到内在的安全和运行管理的效率,在当初学习内存垃圾回收时也重点阐述了这点,互相对照,就更容易加深印象。

版权声明:本文内容由互联网用户自发贡献,该文观点仅代表作者本人。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如发现本站有涉嫌侵权/违法违规的内容,请联系我们,一经查实,本站将立刻删除。
如需转载请保留出处:https://51itzy.com/kjqy/47908.html