在C ++ 11中,按值传递是否是合理的默认值?


142

在传统的C ++中,将值传递给函数和方法对于大型对象而言比较慢,并且通常对此不屑一顾。取而代之的是,C ++程序员倾向于传递引用,这虽然更快,但是却引入了与所有权有关的各种复杂问题,尤其是与内存管理有关的问题(在对象是堆分配的情况下)

现在,在C ++ 11中,我们有了Rvalue引用和move构造函数,这意味着可以实现大对象(例如std::vector),而该对象很容易通过值传入和传出函数。

因此,这是否意味着默认值应为诸如std::vector和类型的实例按值传递std::string?定制对象呢?最新的最佳做法是什么?


22
pass by reference ... which introduces all sorts of complicated questions around ownership and especially around memory management (in the event that the object is heap-allocated)。我不了解所有权的复杂性或问题性?可能我错过了什么吗?
iammilind 2011年

1
@iammilind:个人经验的一个例子。一个线程有一个字符串对象。它被传递给产生另一个线程的函数,但调用者不知道该函数将字符串作为const std::string&副本,而不是副本。然后,第一个线程退出了……
Zan Lynx

12
@ZanLynx:听起来像一个函数,显然从来没有设计成被称为线程函数。
Nicol Bolas

5
同意iammilind,我看不出任何问题。对于“大”对象,默认为通过const引用传递;对于小对象,默认为按值传递。我将大小限制在大约16个字节(或在32位系统上为4个指针)之间。
JN

3
赫伯·萨特回归基础!CppCon 上现代C ++演示的要点对此进行了很多详细介绍。视频在这里
克里斯·德鲁

Answers:


138

如果您需要在体内复制,这是一个合理的默认值。这就是Dave Abrahams 所提倡的

准则:请勿复制函数参数。而是按值传递它们,然后让编译器进行复制。

在代码中,这意味着不要这样做:

void foo(T const& t)
{
    auto copy = t;
    // ...
}

但是这样做:

void foo(T t)
{
    // ...
}

优点是调用方可以foo像这样使用:

T lval;
foo(lval); // copy from lvalue
foo(T {}); // (potential) move from prvalue
foo(std::move(lval)); // (potential) move from xvalue

并且只完成了很少的工作。您需要两个重载才能对引用void foo(T const&);和进行相同操作void foo(T&&);

考虑到这一点,我现在这样写我有价值的构造函数:

class T {
    U u;
    V v;
public:
    T(U u, V v)
        : u(std::move(u))
        , v(std::move(v))
    {}
};

否则,通过引用const仍然是合理的。


29
+1,特别是对于最后一点:)不要忘记,仅当预期要从其移动的对象之后SomeProperty p; for (auto x: vec) { x.foo(p); }不会保持不变时,才可以调用Move构造函数:例如,不合适。另外,Move构造函数具有成本(对象越大,成本越高),而const&实际上是免费的。
Matthieu M.

25
@MatthieuM。但是重要的是要知道此举的“对象越大,成本越高”的含义:“更大”实际上意味着“其具有的成员变量越多”。例如,移动std::vector一百万个元素的成本与移动五个元素的成本相同,因为仅移动指向堆上数组的指针,而不移动向量中的每个对象。因此,实际上这并不是一个大问题。
卢卡斯

+1自从我开始使用C ++ 11以来,我还倾向于使用先传递值,然后移动的构造。这让我感到有些不安,虽然,因为我的代码现在拥有std::move所有的地方..
斯泰恩

1
的风险之一const&,使我绊了好几次。 void foo(const T&); int main() { S s; foo(s); }。即使类型不同,也可以编译,即使有一个以S为参数的T构造函数。这可能很慢,因为可能会构造一个较大的T对象。您可能会认为您传递的参考文献没有抄袭,但也许您是。看到这个问题的答案,我会问更多。基本上,&通常只绑定到左值,但是例外rvalue。还有其他选择。
亚伦·麦克戴德

1
@AaronMcDaid这是个老新闻,从某种意义上说,即使在C ++ 11之前,您也必须始终意识到这一点。在这方面并没有太大变化。
吕克·丹顿

71

在几乎所有情况下,您的语义应为:

bar(foo f); // want to obtain a copy of f
bar(const foo& f); // want to read f
bar(foo& f); // want to modify f

所有其他签名应仅少量使用,并且有充分的理由。现在,编译器将始终以最有效的方式解决这些问题。您可以继续编写代码!


2
虽然如果要修改参数,我更喜欢传递指针。我同意Google样式指南的观点,即这很明显可以修改参数,而无需仔细检查函数的签名(google-styleguide.googlecode.com/svn/trunk/…)。
Max Lybbert

40
我不喜欢传递指针的原因是它向我的函数添加了可能的失败状态。我尝试编写所有函数,以使它们被证明是正确的,因为它大大减少了隐藏错误的空间。foo(bar& x) { x.a = 3; }foo(bar* x) {if (!x) throw std::invalid_argument("x"); x->a = 3;
Ayjay 2011年

22
@Max Lybbert:使用指针参数时,您不需要检查函数的签名,但是您需要检查文档以了解是否允许传递空指针,函数是否具有所有权等。恕我直言,与非常量引用相比,指针参数传达的信息要少得多。但是,我同意,在调用站点上有一个可视线索可以修改参数(如refC#中的关键字)会很不错。
卢·图拉耶

关于按值传递和依赖移动语义,我觉得这三种选择在解释参数的预期用途方面做得更好。这些也是我始终遵循的准则。
Trevor Hickey

1
@AaronMcDaid is shared_ptr intended to never be null? Much as (I think) unique_ptr is?这两个假设都不正确。unique_ptrshared_ptr可以保留null / nullptr值。如果您不想担心null值,则应该使用引用,因为它们永远不能为null。您也不必键入->,您会发现这很烦人:)
朱利安(Julian)

10

如果在函数体内需要对象的副本或仅需要移动对象,则按值传递参数。擦肩而过const&如果你只需要非突变访问对象。

对象复制示例:

void copy_antipattern(T const& t) { // (Don't do this.)
    auto copy = t;
    t.some_mutating_function();
}

void copy_pattern(T t) { // (Do this instead.)
    t.some_mutating_function();
}

对象移动示例:

std::vector<T> v; 

void move_antipattern(T const& t) {
    v.push_back(t); 
}

void move_pattern(T t) {
    v.push_back(std::move(t)); 
}

非变异访问示例:

void read_pattern(T const& t) {
    t.some_const_function();
}

有关基本原理,请参阅Dave AbrahamsXiang Fan的这些博客文章。


0

函数的签名应反映其预期用途。可读性对于优化器也很重要。

这是优化器创建最快的代码的最佳前提条件-至少从理论上讲,如果不是现实的话,则是几年后的现实。

在参数传递的上下文中,性能考虑因素经常被高估。完美转发就是一个例子。像这样emplace_back的函数通常都很短并且内联。

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.