为什么按值传递而不是按常量引用传递?

Why pass by value and not by const reference?

因为 const 引用与按值传递几乎相同,但没有创建副本(据我所知)。那么是否存在需要创建变量副本的情况(因此我们需要使用按值传递)。

  1. 按值传递整数、浮点数和指针等基本数据类型通常速度更快。
  2. 您的函数可能希望在本地修改参数,而不改变传入变量的状态。
  3. C++11 引入了移动语义。要将对象移动到函数参数中,其类型不能是常量引用。

有些情况下你不修改输入,但你仍然需要输入的内部副本,然后你还不如按值取参数。例如,假设您有一个函数 returns 向量的排序副本:

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;
}

现在一个vector的生成、排序、返回的整个过程基本上都可以完成了"in-place"。

成本较低的示例是使用需要在算法过程中修改的计数器或迭代器的算法。同样,按值获取它们允许您直接使用函数参数,而不需要本地副本。

最好的例子可能是复制和交换成语:

C& operator=(C other)
{
    swap(*this, other);
    return *this;
} 

通过值而不是 const 引用获取 other 可以更轻松地编写正确的赋值运算符,避免代码重复并提供强大的异常保证!

迭代器和指针的传递也是按值完成的,因为它使这些算法的编码更加合理,因为它们可以在本地修改它们的参数。否则像 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;
}

A const& 没有通过引用的 const_cast 无法更改,但可以更改。在代码离开编译器 "analysis range" 的任何一点(可能是对不同编译单元的函数调用,或者通过函数指针它无法确定编译时的值)它必须假设引用的值可能已经改变了。

这需要优化。而且它可以让你更难推断代码中可能存在的错误或怪癖:引用是非本地状态,仅在本地状态上运行且不产生副作用的函数非常容易 去推理。让你的代码易于推理是一个很大的好处:与编写代码相比,更多的时间花在维护和修复代码上,并且花在性能上的努力是可替代的(你可以把它花在重要的地方,而不是把时间浪费在微优化上)。

另一方面,一个值需要将值复制到本地自动存储,这是有成本的。

但是如果您的对象复制成本低,并且您不希望出现上述效果,请始终按值获取,因为它使编译器更容易理解函数。

自然只有在价值便宜的时候才可以复制。如果复制成本高昂,或者即使复制成本未知,该成本也应该足以 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 更快。这主要在编写不能或不会优化的接口时很重要,通常是微优化。

就像很多事情一样,这是一种平衡。

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

当您传递一个 const 引用时,您传递的是一个指针(引用是添加了额外糖分以减少苦味的指针)。当然,假设该对象很容易复制。

要访问引用,编译器将不得不取消引用指针以获取内容[假设它不能被内联并且编译器优化取消引用,但在这种情况下,它也会优化额外的副本,因此按价值传递也没有损失]。

因此,如果您的副本比取消引用和传递指针的总和 "cheaper",那么当您按值传递时,您 "win"。

当然,如果您无论如何都要制作一个副本,那么您也可以在构造参数时制作副本,而不是稍后显式复制。