传递值和std :: move优于传递参考


105

我目前正在学习C ++,并尝试避免养成不良习惯。据我了解,clang-tidy包含许多“最佳实践”,并且我会尽力做到最好(即使我不一定理解为什么它们仍然被认为是好的),但是我不确定我是否了解这里的建议。

我从教程中使用了此类:

class Creature
{
private:
    std::string m_name;

public:
    Creature(const std::string &name)
            :  m_name{name}
    {
    }
};

这导致clang-tidy建议我应该按值传递,而不是引用和使用std::move。如果这样做,我得到建议做name一个参考(以确保每次都不会被复制),并且警告是std::move无效的,因为它name是一个警告,const所以我应该删除它。

我没有收到警告的唯一方法是const完全删除:

Creature(std::string name)
        :  m_name{std::move(name)}
{
}

这似乎合乎逻辑,因为的唯一好处const是防止与原始字符串混淆(因为我按值传递,所以不会发生)。但是我在CPlusPlus.com阅读

尽管要注意-在标准库中-移动意味着从中移出的对象处于有效但未指定的状态。这意味着在执行此操作后,仅应销毁移出对象的值或为其分配新值;否则访问它会产生未指定的值。

现在想象一下这段代码:

std::string nameString("Alex");
Creature c(nameString);

因为nameString通过值传递,所以std::move只会name在构造函数内部无效,而不会接触原始字符串。但是,这样做的好处是什么?似乎该内容仅被复制一次-如果我在调用时通过引用传递m_name{name},如果在传递时通过值传递(然后它被移动)。我知道这比按值传递而不使用std::move(因为它被复制两次)更好。

有两个问题:

  1. 我是否正确理解这里发生的事情?
  2. 使用std::move按引用传递和仅调用有任何好处m_name{name}吗?

3
通过参考传递,Creature c("John");制作了一个额外的副本
user253751 '18

1
该链接可能是有价值的读物,它也涵盖了传递std::string_view和SSO。
lubgr

我发现clang-tidy这是让自己沉迷于不必要的微优化的一种好方法,但是却牺牲了可读性。首先要问的问题是,我们实际调用Creature构造函数多少次。
cz

Answers:


36
  1. 我是否正确理解这里发生的事情?

是。

  1. 使用std::move按引用传递和仅调用有任何好处m_name{name}吗?

易于掌握的功能签名,没有任何其他重载。签名立即表明该参数将被复制-这使调用者不必怀疑const std::string&引用是否可以存储为数据成员,以后可能成为悬挂的引用。当将右值传递给函数时,无需重载std::string&& nameconst std::string&参数来避免不必要的复制。传递左值

std::string nameString("Alex");
Creature c(nameString);

按值接受参数的函数将导致一复制和一移动构造。将右值传递给相同的函数

std::string nameString("Alex");
Creature c(std::move(nameString));

导致两次移动构造。相反,当function参数const std::string&为时,即使传递右值参数,也始终会有一个副本。只要参数类型对move-construct便宜(这就是std::string),这显然是一个优势。

但是要考虑一个缺点:对于将函数参数分配给另一个变量的函数(而不是对其进行初始化),该推理不起作用:

void setName(std::string name)
{
    m_name = std::move(name);
}

将导致m_name在重新分配之前重新分配引用的资源。我建议阅读《有效的现代C ++》中的第41项以及这个问题


这是有道理的,尤其是使声明更直观易读。我不确定我是否完全掌握了答案的释放部分(并了解了链接的线程),所以仅检查是否使用move,就可以释放该空间。如果我不使用move,则仅当分配的空间太小而无法容纳新字符串时,它才会被释放,从而提高了性能。那是对的吗?
Blackbot '18年

1
是的,就是这样。m_nameconst std::string&参数分配给时,只要m_name适合,内部存储器就会被重新使用。移动分配时m_name,必须事先释放内存。否则,不可能从任务的右侧“窃取”资源。
lubgr

它什么时候成为悬空参考?我认为初始化列表使用深层复制。
李太极

101
/* (0) */ 
Creature(const std::string &name) : m_name{name} { }
  • 传递的左值绑定到name,然后复制到中m_name

  • 传递的右值绑定到name,然后复制到中m_name


/* (1) */ 
Creature(std::string name) : m_name{std::move(name)} { }
  • 传递的左值复制到中name,然后移动到中m_name

  • 一个通过右值移动name,然后移动m_name


/* (2) */ 
Creature(const std::string &name) : m_name{name} { }
Creature(std::string &&rname) : m_name{std::move(rname)} { }
  • 传递的左值绑定到name,然后复制到中m_name

  • 甲传递右值绑定到rname,然后被移动m_name


由于移动操作通常比副本快,因此,如果您通过大量临时任务,则(1)优于(0)(2)就复制/移动而言是最佳的,但需要代码重复。

完美的转发可以避免代码重复:

/* (3) */
template <typename T,
          std::enable_if_t<
              std::is_convertible_v<std::remove_cvref_t<T>, std::string>, 
          int> = 0
         >
Creature(T&& name) : m_name{std::forward<T>(name)} { }

您可能需要约束T,以限制可以实例化此构造函数的类型的域(如上所示)。C ++ 20旨在通过Concepts简化此过程。


在C ++ 17中,prvalue受到保证的复制保留的影响,在适用的情况下,当将参数传递给函数时,复制保留将减少复制/移动的次数。


对于(1),pr-value和xvalue的情况不相同,因为c ++ 17否?
奥利夫,

1
请注意,在这种情况下,您不需要 SFINAE来完善前进。只需要消除歧义。这是振振有词的潜在错误信息传递错误参数时有用
Caleth

@Oliv是的。xvalues需要移动,而prvalues可以省略:)
Rakete1111 '18

1
我们可以这样写:Creature(const std::string &name) : m_name{std::move(name)} { }(2)
skytree '19

4
@skytree:您不能从const对象中移动,因为移动会更改源。可以编译,但是可以复制。
罗密欧

1

如何传递是不是唯一的变量在这里,什么你传递使得两者之间的天壤之别。

在C ++中,我们有各种值类别,而这种“成语”存在,您在通过箱子右值(如"Alex-string-literal-that-constructs-temporary-std::string"std::move(nameString)),其结果在0拷贝std::string正在取得(类型甚至不必是拷贝构造(用于右值参数),并且仅使用std::string的move构造函数。

与Q&A相关的内容


1

与按(rv)引用传递相比,按值传递和移动方法有几个缺点:

  • 它导致生成3个对象而不是2个。
  • 按值传递对象可能会导致额外的堆栈开销,因为即使常规字符串类通常通常比指针大至少3或4倍;
  • 参数对象的构造将在调用方完成,从而导致代码膨胀;

您能否阐明为什么会导致3个物体产生?据我了解,我可以将“ Peter”作为字符串传递。这将被生成,复制然后移动,不是吗?而且无论在某个时候都不会使用堆栈吗?不是在构造函数调用时,而是在m_name{name}复制它的那一部分?
Blackbot '18年

@Blackbot我指的是您的示例,std::string nameString("Alex"); Creature c(nameString);一个对象是nameString,另一个是函数参数,第三个是类字段。
user7860670

By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.