在原始对象上使用惯用(例如 TBB 的 thread_enumerable_specific')移动赋值调用析构函数

Does idiomatic (e.g. TBB's thread_enumerable_specific') move assignment call destructor on original object

本文关键字:specific enumerable 赋值 移动 析构函数 值调用 thread 对象 原始 TBB 例如      更新时间:2023-10-16

假设我正在使用一个惯用的 Cpp 库(例如英特尔的 TBB),并且在某个类中有一个in-place成员(例如TsCountersType _ts_counters;)。该成员由其默认构造函数自动初始化(如果存在,否则编译错误),除非我自己的KMeans构造函数通过直接调用其构造函数来显式初始化它。

然后,如果我在普通的非构造函数方法中为成员字段分配一个新的(通过调用构造函数创建的)值(例如init),究竟什么是安全的假设?

  1. 我知道在这种情况下,分配的右侧是rvalue的,因此将调用move assignment运算符。
  2. 我认为可以安全地假设行为良好的惯用 Cpp 代码应该调用原始对象的(例如_ts_counters) 析构函数,然后再将新对象移动到内存位置。但这样的假设合理吗?
  3. 默认移动分配运算符呢?它是否在移入之前对原始对象调用析构函数?这甚至是相关的问题,还是编译器仅在(除其他条件外)未定义显式析构函数时才创建默认移动赋值运算符?

    1. 如果是这种情况,如果我有一个类似于 TBB 的场景,而不是TsCountersType我有一个简单的自定义提示,只有一个unique_ptr并默认其他所有内容(构造函数、析构函数、移动赋值......那么unique_ptr什么时候会超出范围呢?
  4. 特别是在TBB的情况下,鉴于他们的文档,我可以假设它发生了:Supported since C++11. Moves the content of other to *this intact. other is left in an unspecified state, but can be safely destroyed.

示例代码:

class KMeans 
{
private:
//...
// thread specific counters
typedef std::pair<std::vector<point_t>, std::vector<std::size_t>> TSCounterType;
typedef tbb::enumerable_thread_specific<TSCounterType> TsCountersType;
TsCountersType _ts_counters;
public:
//...
KMeans() : _tbbInit(tbb::task_scheduler_init::automatic) {}
virtual void init(std::size_t points, std::size_t k, std::size_t iters)
{
// When _ts_counters is replaced by a new object the destructor is automatically called on the original object
_ts_counters = TsCountersType(std::make_pair(std::vector<point_t>(k), std::vector<std::size_t>(k)));
}
};

请考虑以下代码:

#include<iostream>
struct S
{
S() { std::cout << __PRETTY_FUNCTION__ << std::endl;}
S(S const &) { std::cout << __PRETTY_FUNCTION__ << std::endl;}
S(S&&) { std::cout << __PRETTY_FUNCTION__ << std::endl;}
S& operator=(S const &) { std::cout << __PRETTY_FUNCTION__ << std::endl; return *this;}
S& operator=(S&&) { std::cout << __PRETTY_FUNCTION__ << std::endl; return *this;}
~S() { std::cout << __PRETTY_FUNCTION__ << std::endl; }
};
struct W
{
W() : s{} {}
void init() { s = S{}; }
private:
S s;
};
int main()
{
W w{};
w.init();
}

这产生的输出(clang和gcc测试):

S::S()
S::S()
S &S::operator=(S &&)
S::~S()
S::~S()

那么,究竟发生了什么:

  • W的 constcructor 被调用,默认初始化S
  • 默认构造临时S并立即从成员中移动到W::s
  • 临时对象现已销毁(就在 init 退出之前)
  • W::s在主出口处被破坏。