哪种"return"方法更适合 C++/C++11 中的大数据?

Which "return" method is better for large data in C++/C++11?

本文关键字:C++11 数据 C++ 方法 return 哪种      更新时间:2023-10-16

这个问题是由C++11中对RVO的混淆引发的。

我有两种"返回"值的方法:按值返回通过引用参数返回

这是我的示例代码,这两个代码做相同的工作:

按值返回

struct SolutionType
{
    vector<double> X;
    vector<double> Y;
    SolutionType(int N) : X(N),Y(N) { }
};
SolutionType firstReturnMethod(const double input1,
                               const double input2);
{
    // Some work is here
    SolutionType tmp_solution(N); 
    // since the name is too long, I make alias.
    vector<double> &x = tmp_solution.X;
    vector<double> &y = tmp_solution.Y;
    for (...)
    {
    // some operation about x and y
    // after that these two vectors become very large
    }
    return tmp_solution;
}

通过参考参数返回

void secondReturnMethod(SolutionType& solution,
                        const double input1,
                        const double input2);
{
    // Some work is here        
    // since the name is too long, I make alias.
    vector<double> &x = solution.X;
    vector<double> &y = solution.Y;
    for (...)
    {
    // some operation about x and y
    // after that these two vectors become very large
    }
}

以下是我的问题:

  1. 我如何确保在C++11中发生RVO
  2. 如果我们确信RVO发生了,那么在现在的C++编程中,您推荐哪种"返回"方法?为什么
  3. 为什么有些库使用通过引用参数、代码样式或历史原因返回

更新多亏了这些答案,我知道第一种方法在大多数方面都更好。

这里有一些有用的相关链接,可以帮助我理解这个问题:

  1. 如何在C++11中高效地返回大数据
  2. 在C++中,从函数返回向量仍然是一种糟糕的做法吗
  3. 想要速度?传递值

首先,对您正在做的事情的适当技术术语是NRVO。RVO与被退回的临时物品有关:

X foo() {
   return make_x();
}

NRVO是指返回的命名对象:

X foo() {
    X x = make_x();
    x.do_stuff();
    return x;
}

其次,(N)RVO是编译器优化,并不是强制性的。然而,您可以非常确信,如果您使用现代编译器,(N)RVO将被非常积极地使用。

第三,(N)RVO是而不是C++11功能-早在2011年之前就已经存在了。

首先,C++11中有一个move构造函数。因此,如果您的类支持移动语义,那么即使(N)RVO没有发生,它也将从中移动,而不是复制。不幸的是,并不是所有的东西都能在语义上有效地移动。

第五,引用返回是一个可怕的反模式。它确保对象将被有效地创建两次——第一次是作为"空"对象,第二次是在填充数据时——并且它阻止您使用"空"状态不是有效不变量的对象。

SergyA的答案是完美的。如果你听从这个建议,你几乎总是不会出错的。

然而,有一种"结果",最好从调用站点传递对结果的引用。

这是在使用std容器作为循环中的结果缓冲区的情况下发生的。

如果你看一下函数std::getline,你会看到一个例子。

CCD_ 3被设计为从输入流填充CCD_。

每次使用相同的字符串引用调用getline时,都会覆盖字符串的数据。请注意,随着时间的推移(假设随机行长度),有时需要字符串的隐式reserve才能容纳新的长行。然而,比迄今为止最长的线路更短的线路将不需要reserve,因为已经有足够的capacity

想象一个具有以下签名的getline版本:

std::string fictional_getline(std::istream&);

这意味着每次调用函数时都会返回一个新字符串。无论是否发生RVO或NRVO,都需要创建该字符串,如果它比短字符串优化边界长,则需要分配内存。此外,字符串的内存在每次超出作用域时都会被释放。

在这种情况下,以及其他类似情况下,将结果容器作为引用传递会更加高效。

示例:

void do_processing(const std::string& s)
{
    // ...
}
/// @post: in the case of an error, os.bad() == true
/// @post: in the case of no error, os.bad() == false
std::string fictional_getline(std::istream& stream)
{
    std::string result;
    if (not std::getline(stream, result))
    {
        // what to do here?
    }
    return result;
}
// note that buf is re-used which will require fewer and fewer 
// reallocations the more the loop progresses
void fast_process(std::istream& stream)
{
    std::string buf;
    while(std::getline(std::cin, buf))
    {
        do_processing(buf);
    }
}
// note that buf is re-created and destroyed each time around the loop    
void not_so_fast_process(std::istream& stream)
{
    for(;;)
    {
        auto buf = fictional_getline(stream);
        if (!stream) break;
        do_processing(buf);
    }
}

无法确保RVO(或NVRO)发生在C++11中。无论它是否发生,它都与实现质量(例如编译器)有关,而不是由程序员从根本上控制。

移动语义在某些情况下可以用来实现类似的效果,但与RVO不同。

一般来说,我建议使用任何适用于手头数据的返回方法,这是程序员可以理解的。程序员能够理解的代码更容易正确工作。使用晦涩难懂的技术来优化性能(例如,试图迫使NVRO发生)往往会使代码更难理解,因此更容易出错(例如,增加未定义行为的可能性)。如果代码工作正常,但MEASUREMENT显示它缺乏所需的性能,那么可以探索更神秘的技术来提高性能。但是,试图提前(即在任何测量提供需求证据之前)精心手工优化代码被称为"过早优化"是有原因的。

通过引用返回可以避免在函数返回时复制大数据。因此,如果函数返回一个大型数据结构,则通过引用返回可能比通过值返回更有效(通过各种措施)。不过,这是有权衡的-如果基础数据不存在,而其他代码有对它的引用,那么返回对某个东西的引用是危险的(导致未定义的行为)。然而,返回值会使一些代码很难保存对(例如)可能已不存在的数据结构的引用。

编辑:根据注释中的要求,添加引用返回是危险的示例。

   AnyType &func()
   {
       Anytype x;
        // initialise x in some way
       return x;
   };
   int main()
   {
        // assume AnyType can be sent to an ostream this wah
        std::cout << func() << 'n';     // undefined behaviour here
   }

在这种情况下,func()返回一个引用,该引用在返回后不再存在,通常称为悬空引用。因此,该引用的任何使用(在本例中,打印引用的值)都具有未定义的行为。按值返回(即简单地删除&)会返回变量的副本,当调用方尝试使用该副本时,该副本就会存在

未定义行为的原因是func()的返回方式。但是,未定义的行为将发生在调用方(使用引用)中,而不是func()本身。因果之间的分离可能会使bug很难追踪。