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

时间:2018-08-06 10:53:46

标签: c++

此刻我正在学习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无效,因为nameconst,所以我应该将其删除。

我没有收到警告的唯一方法是完全删除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}有什么好处吗?

6 个答案:

答案 0 :(得分:55)

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

  • 传递的 rvalue 绑定到name,然后复制m_name


/* (1) */ 
Creature(std::string name) : m_name{std::move(name)} { }
  • 将传递的 lvalue 复制name,然后移动m_name。< / p>

  • 通过的 rvalue 移动name,然后被移动m_name。< / p>


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

  • 传递的 rvalue 绑定到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中, prvalues guaranteed copy elision的影响,在适用时,它将在将参数传递给函数时减少复制/移动的次数。

答案 1 :(得分:17)

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

是的

  
      
  1. 使用std::move代替通过引用传递并仅调用m_name{name}有什么好处吗?
  2.   

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

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

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

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

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

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

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

将导致m_name所引用资源的重新分配。我建议阅读《有效的现代C ++》中的第41项,也请阅读this question

答案 2 :(得分:1)

传递方式并不是这里唯一的变量,传递的内容使两者之间有很大的区别。

在C ++中,我们有all kinds of value categories,并且当您传入 rvalue (例如"Alex-string-literal-that-constructs-temporary-std::string"std::move(nameString))的情况下,存在此“惯用语”,这会导致产生{strong> 0个副本的std::string(对于rvalue参数,该类型甚至不必是可复制构造的),并且仅使用std::string的move构造函数。

Somewhat related Q&A

答案 3 :(得分:1)

“按值传递”方法相对于“(rv)传递”引用有几个缺点:

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

答案 4 :(得分:0)

在我的情况下,切换为按值传递然后执行std:move会导致Address Sanitizer中出现堆后释放错误。

https://travis-ci.org/github/acgetchell/CDT-plusplus/jobs/679520360#L3165

所以,我已经关闭了它,以及clang-tidy中的建议。

https://github.com/acgetchell/CDT-plusplus/compare/80c96789f0a2...0d78fd63b332

答案 5 :(得分:-2)

我遇到了同样的问题,并用一个很酷的包装类解决了它。

https://stackoverflow.com/a/65357007/4037515

max-width

然后你可以这样使用它:

#pragma pack(push, 1)
template<class T>
class CopyOrMove{
public:
    CopyOrMove(T&&t):m_move(&t),m_isMove(true){}
    
    CopyOrMove(const T&t):m_reference(&t),m_isMove(false){}
    bool hasInstance()const{ return m_isMove; }
    const T& getConstReference() const {
        return *m_reference;
    } 
    T extract() && {
      if (hasInstance())
            return std::move(*m_move);
      else
            return *m_reference;
    }
    
    void fastExtract(T* out) && {
      if (hasInstance())
            *out = std::move(*m_move);
      else
            *out = *m_reference;
    }  
private:
    union
    {
        T* m_move;
        const T* m_reference;
    };
    bool m_isMove;
};
#pragma pack(pop)