为什么在给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 createdn" << std::endl;
std::any a = n1;
std::cout << "n1 assigned to an"<< std::endl;
a = n2;
std::cout << "n2 assigned to an"<< std::endl;
std::cout << "MyStruct objects from beginning and mystruct object held by a are going to be destructedn"<< 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)

在上面的代码中,根据标准,由于我声明了构造函数和析构函数,所以复制构造函数和复制赋值是编译器默认的。我希望看到的顺序:

  1. 如预期:我希望看到我创建的两个对象n1n2的两个构造函数的调用。
  2. 如预期:我将n1分配给a,我期望调用复制构造函数,我将无法看到,因为它是编译器默认的。
  3. 如预期:我将n2分配给a,因此我希望再次调用复制构造函数,并为先前值为mx35的对象包含析构函数。我希望调用的顺序是相反的,所以首先是析构,然后是复制构造。我们还可以清楚地看到,当将n2赋值给a时,为值为mx47的对象调用了析构函数。我不希望看到这个,除非在屏幕后面我的n2对象首先被复制到某种类型的临时对象。
  4. 如预期:我希望a中包含的对象和我创建的n1n2中的2个对象以创建的相反顺序被销毁,因为它们超出了作用域。

更让我困惑的是,当我自实现其他特殊成员函数时,这种行为发生了变化。无论只取消复制构造函数的注释,还是取消其他特殊成员函数的注释,在将n2赋值给a时,输出~MyStruct(47)都会消失。如果上面代码中至少没有注释复制构造函数,则输出如下:

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 the beginning and the mystruct object held by a are going to be destructed
~MyStruct(47)
~MyStruct(47)
~MyStruct(35)

所以我的具体问题如下:

  1. 为什么在第一种情况下为mx47对象调用析构函数?
  2. 为什么在分配n2而不是之前调用复制构造函数之后调用析构函数?
  3. 为什么mx值为47的对象的析构函数在至少添加复制构造函数时突然停止调用?

以上代码已使用c++、Microsoft和Clang编译器编译,适用于c++ 17和c++ 20标准。

指定any的赋值操作符为:

Effects:构造一个类型为any的对象tmp,该对象包含一个由std​::​forward<T>(rhs)tmp.swap(*this)直接初始化的类型为VT的对象。如果抛出异常则不影响。

libstdc++所做的是将any = n2;计算为any = std::any(n2);。这个临时的any在语句的末尾被销毁,这就是你意外的析构函数打印的来源。

现在,any有一个优化:它并不总是分配它所持有的对象。如果类型足够小并且是no-throw move可构造的,则对象将在any中就地构造,从而节省分配。最初,您的类型满足了这一要求。但是,当您显式添加move构造函数时,它不是noexcept,因此必须分配any。然而,这种分配的结果是,对any进行move赋值可以直接接管右边的指针,而不必移动对象并破坏原始对象。因此,在这种情况下,没有析构函数(或move)。

但是如果您添加了noexcept,那么您将同时看到析构函数和move。

最新更新