在 C++/C++11 中,哪种 "return" 方法更适用于大数据?

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

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

我有两种方法来 "return" 值:return 通过值 return 通过参考参数。如果我考虑性能,我更喜欢第一个。由于按值 return 更自然,我可以轻松区分输入和输出。但是,如果我考虑 return 大数据时的效率。我不能决定,因为在C++11中,有RVO。

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

return 按值

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

return 通过参考参数

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. 我如何确保 RVO 发生在 C++11 中?
  2. 如果我们确定发生了RVO,在现在的C++编程中,您推荐哪种"return"方法?为什么?
  3. 为什么有些库通过引用参数、代码风格或历史原因使用return?

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

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

  1. In C++, is it still bad practice to return a vector from a function?
  2. Want Speed? Pass by Value.

首先,您正在做的事情的正确技术术语是 NRVO。 RVO 与临时 returned:

有关
X foo() {
   return make_x();
}

NRVO 指的是被 returned:

命名的对象
X foo() {
    X x = make_x();
    x.do_stuff();
    return x;
}

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

第三,(N)RVO 不是 C++11 特性 - 它早在 2011 年就存在了。

第四,您在 C++11 中拥有的是 move 构造函数。因此,如果您的 class 支持移动语义,它将被移动,而不是被复制,即使 (N)RVO 没有发生。不幸的是,并不是所有的东西都可以有效地进行语义移动。

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

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

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

一般来说,我建议使用适用于手头数据的任何 return 方法,程序员可以理解。程序员可以理解的代码更容易正确工作。使用神秘的技术来优化性能(例如试图强制 NVRO 发生)往往会使代码更难理解,因此更容易出错(例如增加未定义行为的可能性)。如果代码工作正常,但测量显示它缺乏所需的性能,那么可以探索更多神秘的技术来提高性能。但是,出于某种原因,尝试预先亲切地手动优化代码(即在任何测量提供需要的证据之前)被称为 "premature optimisation"。

通过引用返回可以避免函数在 return 上复制大量数据。因此,如果函数是 returning 大型数据结构,returning by reference 比 returning by value 更有效(通过各种措施)。不过,这需要权衡取舍——return如果底层数据不复存在而其他代码引用了它,那么引用某物是危险的(导致未定义的行为)。然而,returning 一个值使得某些代码很难保存对(例如)可能已不复存在的数据结构的引用。

编辑:添加示例,其中 return 引用是危险的,如评论中所要求的。

   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() return 是对在它 return 之后不再存在的东西的引用 - 通常称为悬挂引用。因此,对该引用的任何使用(在这种情况下,打印引用的值)都具有未定义的行为。按值返回(即简单地删除 &)returns 变量的副本,当调用者尝试使用它时它存在。

未定义行为的原因是 func() returns。但是,未定义的行为将发生在调用者(使用引用)中而不是 func() 本身。因果分离会导致错误很难追踪。

SergyA 的回答很完美。如果您遵循该建议,您几乎总是不会出错。

但是有一种 'result' 最好从调用站点传递对结果的引用。

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

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

std::getline 旨在从输入流中填充 std::string 缓冲区。

每次使用相同的字符串引用调用 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);
    }
}