在 C++11 中,常量引用何时优于按值传递?

When is a const reference better than pass-by-value in C++11?

提问人:thor 提问时间:7/3/2014 最后编辑:Communitythor 更新时间:3/25/2017 访问量:20724

问:

我有一些 C++ 之前的 11 代码,其中我使用引用来传递像 ' 很多这样的大参数。示例如下:constvector

int hd(const vector<int>& a) {
   return a[0];
}

我听说使用新的 C++11 功能,您可以按如下方式传递 by 值,而不会影响性能。vector

int hd(vector<int> a) {
   return a[0];
}

例如,这个答案

C++ 11 的移动语义使得按值传递和返回更具吸引力,即使对于复杂的对象也是如此。

上述两个选项在性能方面是相同的,这是真的吗?

如果是这样,什么时候使用选项 1 中的常量引用比选项 2 更好?(即为什么我们仍然需要在 C++11 中使用 const 引用)。

我问的一个原因是,常量引用使模板参数的推导变得复杂,如果与常量引用性能相同,则仅使用按值传递会容易得多。

C C++11

评论

14赞 Praetorian 7/3/2014
对于您展示的示例,按值传递没有意义。通常,如果函数要创建本地副本,请按值传递参数。
1赞 Rapptz 7/3/2014
参考文献如何使模板参数的推导复杂化?const
3赞 Howard Hinnant 7/3/2014
> 上述两个选项在性能方面是相同的吗?不。当客户端传入 prvalue 时,它们可以是相同的。但除此之外,成本却大不相同。
4赞 Rob K 7/4/2014
不要忘记常量正确性的重要性。如果函数不打算修改参数,则使其为 const,如果为 const,则常量引用更有意义。

答:

11赞 cubuspl42 7/3/2014 #1

有很大的不同。你会得到一个内部数组的副本,除非它快要死了。vector

int hd(vector<int> a) {
   //...
}
hd(func_returning_vector()); // internal array is "stolen" (move constructor is called)
vector<int> v = {1, 2, 3, 4, 5, 6, 7, 8};
hd(v); // internal array is copied (copy constructor is called)

C++11 和右值引用的引入改变了返回对象(如向量)的规则 - 现在您可以这样做(无需担心保证副本)。但是,没有改变将它们作为参数的基本规则 - 除非您实际上需要真正的副本,否则您仍然应该通过常量引用来获取它们 - 然后按值获取。

评论

0赞 kfsone 7/10/2014
你如何得出结论,C++11 右值引用和移动缺点不是将它们作为参数?除了将左值引用提升为右值引用以便将其捕获为参数之外,您认为 std::move 的目的究竟是什么?en.cppreference.com/w/cpp/utility/move
0赞 cubuspl42 7/10/2014
@kfsone 通过说“这都是关于......”我的意思是,对于 OP(函数作者),自 C++98 以来,没有关于将向量等对象作为参数的规则发生了变化。不过,关于返回对象的规则发生了变化 - 现在您可以返回矢量,而无需保证昂贵的副本。子例程作者并不关心调用方如何将参数传递子例程。当你通过常量引用来获取向量时,你不在乎调用者是否使用了向量或其他什么。我将编辑我的 anwser 以使其更清晰。move
0赞 cubuspl42 7/10/2014
@kfsone 如何得出结论,C++11 右值引用和移动构造函数是关于将对象作为参数的?您是否因为引入 C++11 而更改了与将对象作为参数相关的 C++03 代码的单个字母?我不认为有必要投反对票(不管你是否反对)。
0赞 kfsone 7/11/2014
我确实有。就像转动它们是可选的一样,接收它们也是可选的。我见过更改函数参数的情况多于返回值的情况。你返回的已经是一个右值,一个临时的。RVO 依赖于它,编译器悄悄地检测到返回值是可以提升的右值。std::move 通常是我只在调用函数时使用的东西 - 我还没有使用过 .return std::move(something)
0赞 kfsone 7/11/2014
std::vector<T> v; v.reserve(count + 2); v.emplace_back(x); v.emplace_back(y); /*...*/ foo(std::move(v));
7赞 zdan 7/3/2014 #2

请记住,如果不传入 r 值,则按值传入将导致完整的副本。因此,一般来说,按值传递可能会导致性能下降。

75赞 Rapptz 7/3/2014 #3

按值传递的一般经验法则是,无论如何,您最终都会制作副本。也就是说,与其这样做,不如这样做:

void f(const std::vector<int>& x) {
    std::vector<int> y(x);
    // stuff
}

在首先传递 const-ref 然后复制它的地方,您应该这样做:

void f(std::vector<int> x) {
    // work with x instead
}

这在 C++03 中部分正确,并且在移动语义中变得更加有用,因为当使用右值调用函数时,复制可能会被逐个传递情况下的移动所取代。

否则,当您只想读取数据时,按引用传递仍然是首选的有效方法。const

评论

15赞 Howard Hinnant 7/3/2014
否则,通过引用不仅很好,而且更受欢迎。强烈禁止按值传递索引和元素。const
0赞 Samuel 7/3/2014
如果你想在移动语义上中继,它应该是。std::vector<int> &&x
2赞 Rapptz 7/3/2014
@Samuel 按值传递可能会调用 prvalues/xvalues 上的移动构造函数和 lvalues 上的复制构造函数。因此,通过右值引用传递参数不会得到相同的语义。
1赞 luk32 7/3/2014
关于您的一般说明,我前段时间在某处读到,通过引用传递内置类型没有多大意义,所以这没有多大意义,后者可能更快,因为复制这些类型比间接传递更快。如果你有知识,你能说点什么吗?const int& xint x
1赞 Elliott 7/8/2014
@luk32 我希望得到同样的东西 - 大多数答案都集中在 const refs 的“标准”应用上 - 但是完全独立的类型呢?就像一个只包含基元的结构。什么时候值得或不值得按值传递这个结构,而不是按常量引用?
3赞 Matthew Lundberg 7/3/2014 #4

你的例子是有缺陷的。C++ 11 不会使用您拥有的代码移动,并且会进行复制。

但是,您可以通过声明函数以接受右值引用,然后传递一个来获得移动:

int hd(vector<int>&& a) {
   return a[0];
}

// ...
std::vector<int> a = ...
int x = hd(std::move(a));

这是假设您不会再次在函数中使用该变量,除非销毁它或为其分配一个新值。在这里,将值强制转换为右值引用,从而允许移动。astd::move

常量引用允许以静默方式创建临时对象。您可以传入适合隐式构造函数的内容,并且将创建一个临时构造函数。典型的例子是将 char 数组转换为 ,但可以转换 。const std::string&std::vectorstd::initializer_list

所以:

int hd(const std::vector<int>&); // Declaration of const reference function
int x = hd({1,2,3,4});

当然,您也可以将临时设备移入:

int hd(std::vector<int>&&); // Declaration of rvalue reference function
int x = hd({1,2,3,4});

评论

4赞 kloffy 7/3/2014
对象是否移动到函数中与参数是否声明为右值引用无关。如果它是临时的,编译器会将对象移动到参数中或完全省略副本(参见 ideone.com/uXCXcq)。
0赞 blackbird 9/21/2015
不能按照第一个代码块的建议调用第一个版本,因为不能将左值绑定到右值引用。因此,使用 实际上不仅仅是一个选项(“或将值转换为右值引用...”),而是强制性的,如果只有 .int hd(vector<int>&& a);aastd::move(a)hd()
9赞 kfsone 7/3/2014 #5

C++ 11 的移动语义使得按值传递和返回更具吸引力,即使对于复杂的对象也是如此。

但是,您给出的样本是按值传递的样本

int hd(vector<int> a) {

所以 C++11 对此没有影响。

即使您正确地声明了“hd”来取右值

int hd(vector<int>&& a) {

它可能比按值传递便宜,但执行成功的移动(而不是可能根本没有效果的简单移动)可能比简单的按引用传递更昂贵。必须构造一个新的,并且它必须拥有 的内容。我们没有必须分配新的元素数组并复制值的旧开销,但我们仍然需要传输 的数据字段。std::movevector<int>avector

更重要的是,在成功移动的情况下,将在此过程中被销毁:a

std::vector<int> x;
x.push(1);
int n = hd(std::move(x));
std::cout << x.size() << '\n'; // not what it used to be

请看以下完整示例:

struct Str {
    char* m_ptr;
    Str() : m_ptr(nullptr) {}
    Str(const char* ptr) : m_ptr(strdup(ptr)) {}
    Str(const Str& rhs) : m_ptr(strdup(rhs.m_ptr)) {}
    Str(Str&& rhs) {
      if (&rhs != this) {
        m_ptr = rhs.m_ptr;
        rhs.m_ptr = nullptr;
      }
    }
    ~Str() {
      if (m_ptr) {
        printf("dtor: freeing %p\n", m_ptr)
        free(m_ptr);
        m_ptr = nullptr;
      }
    }
};

void hd(Str&& str) {
  printf("str.m_ptr = %p\n", str.m_ptr);
}

int main() {
  Str a("hello world"); // duplicates 'hello world'.
  Str b(a); // creates another copy
  hd(std::move(b)); // transfers authority for b to function hd.
  //hd(b); // compile error
  printf("after hd, b.m_ptr = %p\n", b.m_ptr); // it's been moved.
}

作为一般规则:

  • 按平凡对象的值传递,
  • 如果目标需要可变副本,则按值传递,
  • 如果您总是需要制作副本,请按值传递,
  • 对于非平凡的对象,通过常量引用传递,其中查看者只需要看到内容/状态,但不需要对其进行修改,
  • 当目标需要临时/构造值的可变副本时移动(例如 .std::move(std::string("a") + std::string("b")))
  • 当您需要对象状态的局部性,但想要保留现有值/数据并释放当前持有者时,请移动。

评论

1赞 fredoverflow 7/3/2014
你是在暗示右值引用比左值引用 const 慢吗?似乎您在这里混淆了一些概念(右值、右值引用、移动语义)。
3赞 blackbird 9/21/2015
你为什么要在你的规则#5中使用?它不会受到伤害,但您不需要串联的结果。std::move( ... + ... )std::move()