为何赋值给std::any对象时析构函数会被调用?
编写了如下代码,发现赋值给std::any对象时会出现意外的析构函数调用,而显式声明并实现拷贝构造函数后,该析构调用会消失。
#include <iostream> #include <any> struct MyStruct { int mx; MyStruct(int x) : mx{x} { std::cout << "MyStruct(" << x << ")" << std::endl; } // MyStruct(const MyStruct& other) { // std::cout << "Copy constructor (" << other.mx << ")" << std::endl; // mx = other.mx; // } // // MyStruct& operator=(const MyStruct& other) { // std::cout << "Copy assignment (" << other.mx << ")" << std::endl; // mx = other.mx; // return *this; // } // // MyStruct(MyStruct&& other) { // std::cout << "Move constructor (" << other.mx << ")" << std::endl; // mx = other.mx; // } // // MyStruct& operator=(MyStruct&& other) { // std::cout << "Move assignment (" << other.mx << ")" << std::endl; // mx = other.mx; // return *this; // } ~MyStruct() { std::cout << "~MyStruct(" << mx << ")" << "\n"; } }; int main() { MyStruct n1{35}; MyStruct n2{47}; std::cout << "MyStruct objects were created\n" << std::endl; std::any a = n1; std::cout << "n1 assigned to a\n"<< std::endl; a = n2; std::cout << "n2 assigned to a\n"<< std::endl; std::cout << "MyStruct objects from beginning and mystruct object held by a are going to be destructed\n"<< std::endl; }
未显式实现拷贝构造时的输出
MyStruct(35) MyStruct(47) MyStruct objects were created n1 assigned to a ~MyStruct(35) ~MyStruct(47) n2 assigned to a MyStruct objects from the beginning and the mystruct object held by a are going to be destructed ~MyStruct(47) ~MyStruct(47) ~MyStruct(35)
根据C++标准,由于声明了构造函数和析构函数,编译器会默认生成拷贝构造函数和拷贝赋值运算符。预期行为:
- 正常调用n1和n2的构造函数;
- 将n1赋值给a时调用拷贝构造函数(因默认生成无法观测);
- 将n2赋值给a时,应先销毁a中原有mx=35的对象,再调用拷贝构造函数,但实际出现了mx=47的对象析构调用;
- 程序结束时,a中的对象、n1和n2按创建逆序析构。
显式实现拷贝构造后的输出
MyStruct(35) MyStruct(47) MyStruct objects were created Copy constructor (35) n1 assigned to a Copy constructor (47) ~MyStruct(35) n2 assigned to a MyStruct objects from beginning and mystruct object held by a are going to be destructed ~MyStruct(47) ~MyStruct(47) ~MyStruct(35)
具体问题
- 第一种情况中,mx=47的对象析构函数为何被调用?
- 赋值n2给a时,析构函数为何在拷贝构造之后调用而非之前?
- 为何至少添加拷贝构造函数后,mx=47的对象析构调用会消失?
(注:代码已在g++、MSVC、Clang编译器的C17和C20标准下编译验证)
解答
问题1:mx=47的析构调用来源
当你没有显式定义拷贝构造函数时,编译器生成的拷贝构造是隐式声明的。std::any的实现在处理这种隐式声明拷贝构造的类型时,无法直接在其内部存储中使用placement new构造新对象,而是会先创建一个临时的MyStruct对象(拷贝n2得到mx=47的实例),再将这个临时对象拷贝到std::any的内部存储中。临时对象完成使命后被销毁,就产生了~MyStruct(47)的调用。而~MyStruct(35)则是销毁std::any中原先持有的n1拷贝。
问题2:析构在拷贝构造之后的原因
这是std::any赋值操作的异常安全策略:先构造新的对象(无论是临时对象还是在内部存储中构造),确保新对象构造成功后,再销毁旧对象。如果新对象构造时抛出异常,旧对象仍能保持有效状态,避免程序陷入不一致的错误状态。这种"先构造、后销毁"的顺序是标准库容器和智能指针等类型的常见安全实践。
问题3:显式拷贝构造消除额外析构的原因
当你显式实现拷贝构造函数后,该函数变为用户声明的特殊成员函数。std::any的类型特性检测会识别到这一点,允许直接在其已分配的内部存储中使用placement new拷贝构造新的MyStruct对象,无需创建临时对象。此时赋值流程变为:先拷贝构造新对象到内部存储,再销毁旧对象,因此不会产生临时对象的额外析构调用。
内容的提问来源于stack exchange,提问作者MasterMind

