减少临时对象到就地施工的分配
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 以来,编译器进行赋值的最直接方式是:
- 摧毁
X
- 构造
std::list
- 将
std::list
移动到X
现在,不允许编译器执行以下操作:
- 消灭X
- 就地构建
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
成员函数会好得多。通过使用 assign
,X
有机会重用现有分配。这几乎肯定会使它比您的 "destroy-plus-construct" 版本更快。将链表移动到另一个对象的成本是微不足道的;必须销毁所有这些分配只是为了再次分配它们的成本不是。
这对 std::list
尤其重要,因为它有 手 的分配。
最坏的情况下,assign
的效率将不亚于您从 class 之外想出的任何其他方法。最好的情况是,它会好得多。
您实际上可以通过定义 operator= 来获取初始化列表来实现。
对于std::list,只需调用
X = {"foo","bar","dead","beef"}.
在你的例子中,实际发生的事情是:
- 建造一个临时的
- 在 X 上使用临时变量调用移动赋值运算符
在大多数对象上,例如 std::list,与简单地构建一个对象相比,这实际上并不昂贵。
然而,它仍然会导致对第二个 std::list 的内部存储进行额外分配,这是可以避免的:如果可能的话,我们可以重用已经为 X 分配的内部存储。发生的事情是:
- 构造:临时分配一些space给元素
- 移动:指针移动到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" 也适用于你的第一个例子,你得出相反的结论,即那里有一个自相矛盾的地方。
以std::list
支持移动语义为例。
std::list<std::string> X;
... //X is used in various ways
X=std::list<std::string>({"foo","bar","dead","beef"});
自 C++11 以来,编译器进行赋值的最直接方式是:
- 摧毁
X
- 构造
std::list
- 将
std::list
移动到X
现在,不允许编译器执行以下操作:
- 消灭X
- 就地构建
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
成员函数会好得多。通过使用 assign
,X
有机会重用现有分配。这几乎肯定会使它比您的 "destroy-plus-construct" 版本更快。将链表移动到另一个对象的成本是微不足道的;必须销毁所有这些分配只是为了再次分配它们的成本不是。
这对 std::list
尤其重要,因为它有 手 的分配。
最坏的情况下,assign
的效率将不亚于您从 class 之外想出的任何其他方法。最好的情况是,它会好得多。
您实际上可以通过定义 operator= 来获取初始化列表来实现。 对于std::list,只需调用
X = {"foo","bar","dead","beef"}.
在你的例子中,实际发生的事情是:
- 建造一个临时的
- 在 X 上使用临时变量调用移动赋值运算符
在大多数对象上,例如 std::list,与简单地构建一个对象相比,这实际上并不昂贵。
然而,它仍然会导致对第二个 std::list 的内部存储进行额外分配,这是可以避免的:如果可能的话,我们可以重用已经为 X 分配的内部存储。发生的事情是:
- 构造:临时分配一些space给元素
- 移动:指针移动到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" 也适用于你的第一个例子,你得出相反的结论,即那里有一个自相矛盾的地方。