为什么要传值而不是传const引用?

9

因为 const 引用与传递值基本相同,但不会创建副本(据我所知)。所以有没有需要创建变量副本的情况(这样我们就需要使用按值传递)。


在C++11中,你可以使用按值传递来“吞噬”一个参数。我会搜索并添加一个链接来解释这是什么意思。 - bolov
https://dev59.com/NGAf5IYBdhLWcg3wizIE - bolov
啊,找到了:https://dev59.com/qWkv5IYBdhLWcg3w9lai - bolov
谢谢。我会阅读它,但对我来说有点难 :P - Johnson
请参阅标准库算法,重点是采用函数对象:为什么模板化的函数对象作为值传递而不是转发引用 - underscore_d
5个回答

15

有些情况下,你不需要修改输入参数,但仍然需要它的一个内部副本,此时你可以考虑通过值传递参数。比如说,假设你有一个函数可以返回已排序的向量副本:

template <typename V> V sorted_copy_1(V const & v)
{
    V v_copy = v;
    std::sort(v_copy.begin(), v_copy.end());
    return v;
}

这样做没有问题,但如果用户有一个向量,他们从未需要用于任何其他目的,则必须在此处进行强制复制,这可能是不必要的。因此,只需按值传递参数:

template <typename V> V sorted_copy_2(V v)
{
    std::sort(v.begin(), v.end());
    return v;
}

现在,生成、排序和返回向量的整个过程基本上可以“原地”完成。

较便宜的示例是算法,它们消耗计数器或迭代器,在算法过程中需要修改它们。同样,通过值传递这些参数允许您直接使用函数参数,而不需要创建局部副本。


6
  1. 通常情况下,传递基本数据类型如整型、浮点型和指针时,通过值传递更快。
  2. 您的函数可能想要本地修改参数,而不改变传入变量的状态。
  3. C++11引入了移动语义。将对象移动到函数参数中时,其类型不能为const引用。

谢谢我的朋友。另外,在第三个答案中,这是否意味着旧代码需要更新,因为C++11不允许使用const引用和对象? - Johnson
@Johnson 不是的。大多数旧代码在C++11中应该是向后兼容的。 - Neil Kirk

5

像许多事情一样,这是一个平衡问题。

我们通过const引用传递以避免复制对象。

当您传递const引用时,您传递了一个指针(引用是具有额外糖分的指针,使它们口感不那么苦)。当然,假设对象是可以轻松复制的。

要访问引用,编译器必须解引用指针以获取内容[假设它无法内联并且编译器优化掉解引用,但在这种情况下,它也将优化掉额外的复制,因此从按值传递中没有损失]。

因此,如果您的复制比解引用和传递指针的总和“便宜”,则当您按值传递时,您会“赢得胜利”。

当然,如果您无论如何都要进行复制,那么最好在构造参数时进行复制,而不是稍后显式复制。


3
最好的例子可能是复制和交换惯用语:
C& operator=(C other)
{
    swap(*this, other);
    return *this;
} 

other 通过值传递而不是通过 const 引用传递,可以更轻松地编写正确的赋值运算符,避免代码重复并提供强异常保证!
此外,传递迭代器和指针也是通过值进行的,因为这使得这些算法更加合理易懂,它们可以在本地修改其参数。否则,诸如 std::partition 这样的算法必须立即复制其输入,这既低效又看起来很傻。我们都知道避免看起来傻的代码是第一要务。
template<class BidirIt, class UnaryPredicate>
BidirIt partition(BidirIt first, BidirIt last, UnaryPredicate p)
{
    while (1) {
        while ((first != last) && p(*first)) {
            ++first;
        }
        if (first == last--) break;
        while ((first != last) && !p(*last)) {
            --last;
        }
        if (first == last) break;
        std::iter_swap(first++, last);
    }
    return first;
}

1
一个 const& 引用不能在没有通过 const_cast 改变其值的情况下进行更改,但是它可以被更改。在代码离开编译器的“分析范围”的任何时候(可能是调用不同编译单元的函数,或通过函数指针无法在编译时确定其值),它必须假定所引用的值可能已经改变。
这会导致优化成本增加。它也可能使你在推理代码中可能存在的错误或怪癖方面更加困难:引用是非局部状态,而仅操作局部状态并且产生无副作用的函数则非常容易推理。使你的代码易于推理是一个巨大的好处:花费在维护和修复代码上的时间比编写代码的时间还要多,而在性能方面花费的努力是可替代的(你可以将它花在有意义的地方,而不是在各个地方浪费时间进行微小的优化)。
另一方面,值需要将值复制到本地自动存储中,这是有成本的。
但是,如果你的对象很容易复制,并且你不希望发生上述效果,那么总是按值传递参数,因为这会使编译器更容易理解该函数。
自然地,只有当值便于复制时才采用按值传递。如果复制成本昂贵,或者即使不知道复制成本,那么应该采用const&来承担这个成本。
以上的简短版本是:按值传递可以使您和编译器更容易地推理参数的状态。
还有另一个原因。如果您的对象便宜移动,并且您将要存储本地副本,那么按值传递会打开效率。如果您通过const&获取一个std::string,然后创建一个本地副本,则可能需要创建一个std::string以传递该参数,另一个则为本地副本。
如果您按值获取std::string,则只会创建一个副本(并可能被移动)。
以下是一个具体的例子:
std::string some_external_state;
void foo( std::string const& str ) {
  some_external_state = str;
}
void bar( std::string str ) {
  some_external_state = std::move(str);
}

然后我们可以进行比较:
int main() {
  foo("Hello world!");
  bar("Goodbye cruel world.");
}

调用foo会创建一个包含"Hello world!"std::string,然后再将其复制到some_external_state中。总共创建了2个副本,丢弃了1个字符串。
调用bar直接创建了std::string参数。然后将其状态移动到some_external_state中。总共创建了1个副本,进行了1次移动,丢弃了1个(空)字符串。
此技术还可以提高某些异常安全性能,因为任何分配都发生在bar之外,而foo可能会抛出资源耗尽的异常。
只有当完美转发很麻烦或失败时,移动已知是便宜的,复制可能很昂贵,并且您几乎肯定要对参数进行本地副本时,才适用于此技术。
最后,某些小型类型(如int)的非优化ABI直接复制比const&参数的非优化ABI更快。这主要涉及编写无法或不会进行优化的接口,并且通常是微观优化。

@Johnson 对于那些复制成本较高且可能不可移动的东西,几乎总是应该使用const&引用。而那些复制成本较低的东西,几乎总是应该按值传递。而介于两者之间的东西则需要认真思考。 - Yakk - Adam Nevraumont
@Johnson 普遍的规则是通过const引用传递类类型,其他一切通过值传递。这并不总是最优的,但任何最优解都取决于类型的内部知识(复制的成本如何)或调用的函数。这个规则对于大多数情况来说已经足够好,并且易于遵循。 - James Kanze
@Johnson 当编写一个函数接口,需要传入类型为 Foo 的实例时,通常 你知道 Foo 是什么以及你正在做什么(大致上)。你是在制作本地副本吗?Foo 是否可以便宜地移动?那就按值传递。Foo 是否是一种便宜的复制类型?那就按值传递。你是否真的希望外部 Foo 的更改传递到函数中间的参数中?那就按 const& 传递。Foo 是否很难复制且没有简单的 move?那就按 const& 传递。只有少量具体参数能通过上述测试组合。 - Yakk - Adam Nevraumont
@James 我会认为“易于复制”,“易于移动”,“难以复制”和“无法移动”是某种类型的基本特性,如果你在编写以该类型作为参数的函数,那么你应该了解它们。 - Yakk - Adam Nevraumont
@Yakk 你怎么能这样做呢?这些都是内部细节;在一台机器上便宜复制可能在另一台机器上很昂贵。如果你是一个类的客户端,你不应该知道这些;它们与类的基本语义无关。 - James Kanze
显示剩余5条评论

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接