减少临时对象到就地施工的分配

Reducing assignment of temporary object to in-place construction

std::list支持移动语义为例。

std::list<std::string> X;
... //X is used in various ways
X=std::list<std::string>({"foo","bar","dead","beef"});

自 C++11 以来,编译器进行赋值的最直接方式是:

  1. 摧毁X
  2. 构造std::list
  3. std::list移动到X

现在,不允许编译器执行以下操作:

  1. 消灭X
  2. 就地构建std::list

因为虽然这显然节省了另一个 memcpy 但它消除了赋值。使第二种行为成为可能和可用的便捷方法是什么?是否计划在未来的 C++ 版本中使用?

我的猜测是 C++ 仍然不提供,除了写作:

X.~X();
new(&X) std::list<std::string>({"foo","bar","dead","beef"});

我说得对吗?

Is it planned in future versions of C++?

没有。谢天谢地。

赋值不同于先销毁再创建。 X 在您的作业示例中未被破坏。 X 是活对象; X 内容 可能会被破坏,但 X 本身永远不会被破坏。而且也不应该

如果你想销毁X,那么你有那个能力,使用explicit-destructor-and-placement-new。尽管由于 const 成员的可能性,如果您想要安全,您还需要清洗指向该对象的指针。但是赋值应该永远不会被认为等同于那个。

如果您关心效率,使用 assign 成员函数会好得多。通过使用 assignX 有机会重用现有分配。这几乎肯定会使它比您的 "destroy-plus-construct" 版本更快。将链表移动到另一个对象的成本是微不足道的;必须销毁所有这些分配只是为了再次分配它们的成本不是。

这对 std::list 尤其重要,因为它有 的分配。

最坏的情况下,assign 的效率将不亚于您从 class 之外想出的任何其他方法。最好的情况是,它会好得多。

您实际上可以通过定义 operator= 来获取初始化列表来实现。 对于std::list,只需调用

    X = {"foo","bar","dead","beef"}.

在你的例子中,实际发生的事情是:

  1. 建造一个临时的
  2. 在 X 上使用临时变量调用移动赋值运算符

在大多数对象上,例如 std::list,与简单地构建一个对象相比,这实际上并不昂贵。

然而,它仍然会导致对第二个 std::list 的内部存储进行额外分配,这是可以避免的:如果可能的话,我们可以重用已经为 X 分配的内部存储。发生的事情是:

  1. 构造:临时分配一些space给元素
  2. 移动:指针移动到X; X之前使用的space被释放

有些对象会重载赋值运算符以获取初始化列表,std::vector and std::list 就是这种情况。这样的算子可能会使用内部已经分配的存储,这是这里最有效的解决方案。

// 请在这里插入关于过早优化的通常的杂乱无章

当你有一个涉及移动赋值的语句时:

x = std::move(y);

在移动之前 x 没有调用析构函数。但是,在移动之后,在某些时候会调用 y 的析构函数。移动赋值运算符背后的想法是它可能能够以简单的方式将 y 的内容移动到 x(例如,将指向 y 的存储的指针复制到x)。它还必须确保其以前的内容被正确销毁(它可能选择将其与 y 交换,因为你知道 y 可能不再使用,并且 y 的析构函数将被调用)。

如果移动分配是内联的,编译器可能能够推断出将存储从 y 移动到 x 所需的所有操作都等同于就地构造。

最后一个问题

Am I right?

没有

您关于允许或不允许的想法是错误的。允许编译器替换任何优化,只要它保留可观察到的效果。这称为 "as if" 规则。可能的优化包括删除所有这些代码,如果它不影响任何可观察到的东西的话。特别是,你对第二个例子的 "isn't allowed" 是完全错误的,推理 "it eliminates assignment" 也适用于你的第一个例子,你得出相反的结论,即那里有一个自相矛盾的地方。