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

Why pass by value and not by const reference?

本文关键字:常量 引用 为什么      更新时间:2023-10-16

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

在某些情况下,您不修改输入,但您仍然需要输入的内部副本,然后您也可以按值获取参数。例如,假设您有一个返回向量的排序副本的函数:

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

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

较便宜的例子是使用计数器或迭代器的算法,这些计数器或迭代器需要在算法过程中进行修改。同样,按值获取这些值允许您直接使用 function 参数,而不需要本地副本。

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

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

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

当你传递一个 const 引用时,你传递一个指针(引用是带有额外糖的指针,使它们味道不那么苦)。当然,假设对象是微不足道的复制。

要访问引用,

编译器必须取消引用指针才能访问内容 [假设它不能内联并且编译器优化了取消引用,但在这种情况下,它也会优化掉额外的副本,因此也不会因按值传递而造成损失]。

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

当然,如果你无论如何都要做一个副本,

那么你也可以在构造参数时做一个副本,而不是在以后显式复制。

最好的例子可能是 Copy and Swap 习惯用法:

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

按值而不是常量引用获取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;
}
如果没有

通过引用const_cast,就无法更改const&,但可以更改。 在代码离开编译器的"分析范围"的任何时候(可能是对不同编译单元的函数调用,或者通过函数指针它无法确定编译时的值),它必须假定引用的值可能已更改。

这成本优化。 而且它会使推理代码中可能存在的错误或怪癖变得更加困难:引用是非本地状态,并且仅在本地状态上运行并且不会产生副作用的函数实际上很容易推理。 让你的代码易于推理是一个很大的福音:维护和修复代码的时间比编写代码的时间多,花在性能上的努力是可以替代的(你可以把它花在重要的地方,而不是浪费时间在任何地方的微优化上)。

另一方面,值

要求将该值复制到本地自动存储中,这会产生成本。

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

当然,只有当价值便宜时才能复制。 如果复制成本昂贵,或者即使复制成本未知,该成本也应该足以接受const&

上述 : 按值获取的简短版本使您和编译器更容易推理参数的状态。

还有另一个原因。 如果您的对象移动成本低,并且无论如何您都要存储本地副本,则按价值获取可以提高效率。 如果您通过 const& 获取std::string,然后创建一个本地副本,可以创建一个std::string以传递 s 参数,另一个为本地副本创建。

如果按值获取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 更快。 这主要在编码无法或不会优化的接口时很重要,并且通常是一种微优化。