当前位置: 首页 > 知识库问答 >
问题:

为什么我们复制然后移动?

党祖鹤
2023-03-14

我看到一些代码,其中有人决定复制一个对象,然后将其移动到类的数据成员。这让我感到困惑,因为我认为搬家的目的就是为了避免抄袭。以下是一个例子:

struct S
{
    S(std::string str) : data(std::move(str))
    {}
};

以下是我的问题:

  • 为什么我们不把右值引用到str
  • 拷贝不是很贵吗,特别是考虑到像std::string
  • 是什么原因让作者决定复制然后移动
  • 我应该什么时候自己做

共有3个答案

卢英叡
2023-03-14

这可能是有意的,类似于复制和交换习惯用法。基本上,因为字符串是在构造函数之前复制的,所以构造函数本身是异常安全的,因为它只交换(移动)临时字符串str。

冀俊良
2023-03-14

为了理解为什么这是一个好的模式,我们应该检查C03和C11中的替代方案。

我们有一个C03方法来获取std::字符串常量

struct S
{
  std::string data; 
  S(std::string const& str) : data(str)
  {}
};

在这种情况下,将始终执行单个副本。如果从原始C字符串构造,将构造一个std::string,然后再次复制:两个分配。

有一种C 03方法,可以引用一个std::string,然后将其交换为本地std::string

struct S
{
  std::string data; 
  S(std::string& str)
  {
    std::swap(data, str);
  }
};

这就是C 03版本的“移动语义”,而且swap通常可以优化为非常便宜的操作(很像move)。还应结合以下背景进行分析:

S tmp("foo"); // illegal
std::string s("foo");
S tmp2(s); // legal

并强制您形成一个非临时的std::字符串,然后丢弃它。(临时std::字符串不能绑定到非const引用)。然而,只完成了一次分配。C 11版本需要一个

struct S
{
  std::string data; 
  S(std::string&& str): data(std::move(str))
  {}
};

使用:

S tmp("foo"); // legal
std::string s("foo");
S tmp2(std::move(s)); // legal

接下来,我们可以制作完整的C 11版本,它支持复制和移动

struct S
{
  std::string data; 
  S(std::string const& str) : data(str) {} // lvalue const, copy
  S(std::string && str) : data(std::move(str)) {} // rvalue, move
};

然后我们可以检查如何使用它:

S tmp( "foo" ); // a temporary `std::string` is created, then moved into tmp.data

std::string bar("bar"); // bar is created
S tmp2( bar ); // bar is copied into tmp.data

std::string bar2("bar2"); // bar2 is created
S tmp3( std::move(bar2) ); // bar2 is moved into tmp.data

很明显,这种2重载技术至少与上述两种C 03样式一样有效(如果不是更有效的话)。我将把这个2-重载版本称为“最佳”版本。

现在,我们将检查复制版本:

struct S2 {
  std::string data;
  S2( std::string arg ):data(std::move(x)) {}
};

在每种情况下:

S2 tmp( "foo" ); // a temporary `std::string` is created, moved into arg, then moved into S2::data

std::string bar("bar"); // bar is created
S2 tmp2( bar ); // bar is copied into arg, then moved into S2::data

std::string bar2("bar2"); // bar2 is created
S2 tmp3( std::move(bar2) ); // bar2 is moved into arg, then moved into S2::data

如果您将此并排与“最优化”版本进行比较,我们只做了一个额外的移动!我们没有一次做额外的复制

因此,如果我们假设move价格便宜,那么这个版本可以让我们获得与最佳版本几乎相同的性能,但代码减少了2倍。

如果你考虑2到10个参数,代码的减少是指数级的——1个参数减少2倍,2个参数减少4倍,3个参数减少8倍,4个参数减少16倍,10个参数减少1024倍。

现在,我们可以通过perfect forwarding和SFINAE来解决这个问题,它允许您编写一个包含10个参数的构造函数或函数模板,并执行SFINAE以确保参数具有适当的类型,然后根据需要将其移动或复制到本地状态。虽然这可以防止程序大小增加千倍,但仍然可以从该模板生成一大堆函数。(模板函数实例化生成函数)

大量生成的函数意味着更大的可执行代码大小,这本身会降低性能。

由于花费了几个移动,我们得到了更短的代码和几乎相同的性能,并且通常更容易理解代码。

现在,这只是因为我们知道,当调用函数(在本例中是构造函数)时,我们需要该参数的本地副本。这个想法是,如果我们知道我们将要制作一个副本,我们应该通过把它放在我们的参数列表中,让调用方知道我们正在制作一个副本。然后,他们可以围绕他们将给我们一个副本这一事实进行优化(例如,通过进入我们的论点)。

“按值获取”技术的另一个优点是,move构造函数通常是noexcept。这意味着按值获取并移出参数的函数通常可以是noexcept,将任何throws移出其主体并进入调用范围(有时谁可以通过直接构造来避免它,或者构造项并将其移动到参数中,以控制抛出的位置)。使方法无效通常是值得的。

呼延鸿畅
2023-03-14

在我回答你的问题之前,有一件事你似乎弄错了:在C11中按价值获取并不总是意味着复制。如果传递了右值,它将被移动(如果存在可行的移动构造函数),而不是被复制。d::字符串确实有一个移动构造函数。

与C 03不同,在C 11中,通常习惯于按值获取参数,原因我将在下面解释。也看到这个Q

为什么我们不把右值引用到str

因为这样就不可能传递左值,例如:

std::string s = "Hello";
S obj(s); // s is an lvalue, this won't compile!

如果S只有一个接受右值的构造函数,则不会编译上述内容。

副本不会很贵吗,尤其是考虑到像std::字符串这样的东西?

如果您传递一个右值,它将被移动到str,并且最终将被移动到数据中。不会执行任何复制。另一方面,如果传递左值,该左值将被复制到str,然后移动到数据中。

总之,右值移动两次,左值移动一次,复制一次。

是什么原因让作者决定复制然后移动?

首先,正如我上面提到的,第一个并不总是一个副本;也就是说,答案是:“因为它是高效的(std::string对象的移动很便宜)和简单的”。

假设移动是便宜的(这里忽略SSO),在考虑此设计的总体效率时,实际上可以忽略它们。如果我们这样做,我们有一个左值副本(如果我们接受对常量的左值引用,我们会有一个副本),而没有右值副本(如果我们接受对常量的左值引用,我们仍然会有一个副本)。

这意味着,当提供左值时,按值获取和按左值引用const一样好,当提供右值时更好。

附言:为了提供一些背景,我相信这是Q

 类似资料:
  • 问题内容: 为什么编译器没有在开关中的每个代码块之后自动放置break语句?是出于历史原因吗?您何时要执行多个代码块? 问题答案: 有时将多个案例与同一代码块关联会很有帮助,例如 等。只是一个例子。 以我的经验,通常“摔倒”并在一种情况下执行多个代码块是不好的风格,但是在某些情况下可能会有用处。

  • 问题内容: 我是一个完整的初学者。 我已阅读了有关解决方案的Google文档。我在互联网上搜索了同样的内容。 但。一切似乎都是技术性的。 据我了解,.Flush有助于在功能出现时立即执行这些功能,而无需将它们捆绑在一起。 我对吗? 如果不是的话,外行人的含义是什么?并请举一个简单的例子。谢谢。 问题答案: 程序员在希望确保在继续之前将先前代码的输出和/或效果写入电子表格时会使用。如果您不这样做,则

  • 我们必须用分机写这行。java,尽管它的扩展是。kt我认为Kotlin文件会转换成java文件,但是java也会转换成字节码,所以我们也可以使用。类文件,如果Kotlin代码转换为java代码。 到 所以问题是我们为什么要写这个。java在 问题由此产生。

  • 我需要在Go中复制一个切片,并读取文档。有一个复制功能可供我使用。 copy内置函数将元素从源片复制到目标片。(作为一种特殊情况,它还会将字节从字符串复制到字节片。)源和目标可能重叠。Copy返回复制的元素数,它是len(src)和len(dst)中的最小值。 但当我这样做的时候: 我的和以前一样是空的(我甚至尝试使用): 你可以去游乐场看看。那么为什么我不能复制一个切片呢?

  • 所以我在JavaScript中使用了

  • 我今天想使用,但出现了如下错误: 所以我想我应该清除并重新安装(如下所示)https://www.rosehosting.com/blog/how-to-install-pip-on-ubuntu-16-04/)但仍然得到: 我怎样才能提高我的水平