【问题标题】:How does C++ return value optimization (RVO) work when the function get's called with the variable that the return value gets assigned to?当使用分配返回值的变量调用函数 get 时,C++ 返回值优化 (RVO) 如何工作?
【发布时间】:2021-01-07 01:26:22
【问题描述】:

最初,我遇到了这样的问题:我有一个带有数据的向量,并且想要执行 n 次操作。就地执行它是不可能的,因此在每个循环周期中都会构造一个新向量,操作完成并释放内存。什么样的操作对我的问题并不重要,但在数学上它是一个排列的平方(我只是想提出一个无法就地完成的操作)。它将result[i] = in[in[i]] 应用于所有元素。

vector<int> *sqarePermutationNTimesUnsafe(vector<int> *in, int n)
{
    if (n <= 0) { throw; }
    
    vector<int> *result = in;
    vector<int> *shuffled;
    for (int i = 0; i < n; i++)
    {
        shuffled = new vector<int>(in->size(), 0);
        for (int j = 0; j < in->size(); j++)
        {
            (*shuffled)[j] = (*result)[(*result)[j]];
        }

        if (result != in) { delete result; }
        result = shuffled;
    }
    return result;
}

加速的主要内容是,新数据被写入shuffled,然后只需要进行下一次洗牌所需的指针交换。它可以工作,但它很丑陋且容易出错。所以我想一个更好的方法是使用现代 C++。传递引用应该比传递vectors&lt;...&gt; 更快,所以我这样做了。由于无法直接交换引用,因此我将其拆分为两个函数,并依靠返回值优化来交换引用。

// do the permutation
vector<int> sqarePermutation(const vector<int> &in)
{
    vector<int> result(in.size(), 0);
    for (int i = 0; i < in.size(); i++)
    {
        result[i] = in[in[i]];
    }
    return result;
}

// do the permutation n times
vector<int> sqarePermutationNTimes(const vector<int> &in, const int n)
{
    vector<int> result(in); // Copying here is ok and required
    for (int i = 0; i < n; i++)
    {
        result = sqarePermutation(result); // Return value optimization should be used so "swap" the references
    }
    return result;
}

我想确保 RVO 可以正常工作,所以我编写了一个小程序来测试它。

#include <iostream>

using namespace std;

class RegisteredObject
{
private:
    int index;
public:
    RegisteredObject(int index);
    ~RegisteredObject();
    int getIndex() const;
};

RegisteredObject::RegisteredObject(int index): index(index)
{
    cout << "- Register Object " << index << endl;
}

RegisteredObject::~RegisteredObject()
{
    cout << "- Deregister Object " << index << endl;
}

int RegisteredObject::getIndex() const
{
    return index;
}

RegisteredObject objectWithIncreasedIndex(const RegisteredObject &object)
{
    return RegisteredObject(object.getIndex() + 1);
}


int main() {
    cout << "Init a(0)" << endl;
    RegisteredObject a(0); 
    cout << "RVO from a to a" << endl;
    a = objectWithIncreasedIndex(a); // Seems to be buggy
    cout << "RVO from a to b" << endl;
    RegisteredObject b = objectWithIncreasedIndex(a); // Why does a get destructed here?
    cout << "End" << endl;
    return 0;
}

不要犹豫,在您的机器上尝试一下,结果可能会有所不同。该程序有一个简单的数据对象,显示何时调用构造函数和析构函数。我正在使用针对 x86_64-apple-darwin19.6.0 的 Mac OS Clang 11.0.3。它产生以下结果:

Init a(0)
- Register Object 0
RVO from a to a
- Register Object 1
- Deregister Object 1 //EDIT: <--- this should be Object 0
RVO from a to b
- Register Object 2
End
- Deregister Object 2
- Deregister Object 1

如您所见,对象 1 永远不会被破坏。我认为这是因为 RVO。 RVO 将新的对象 1 构造到对象 0 的位置。但是因为它忘记制作对象 0 的临时副本,所以使用索引 1 调用析构函数。

将索引声明为 const 有助于防止此错误,因为编译器会抛出错误。

object of type 'RegisteredObject' cannot be assigned because its copy
      assignment operator is implicitly deleted

但我认为这不是解决方案。对我来说,C++(或至少 clang 的)RVO 似乎坏了。这意味着,上述 Permutation 的实现可能会尝试双重释放内存,或者根本不使用 RVO。

首先,您认为是什么导致了未释放对象 1 的错误?

您将如何实现sqarePermutationNTimes 方法的高效且美观的版本?

【问题讨论】:

  • 不,返回值优化不会“交换引用”,如评论。这不是返回值优化的内容。
  • throw; 没有表达式会表现出未定义的行为,除非在 catch 处理程序中直接或间接执行。
  • a = objectWithIncreasedIndex(a); 通过赋值运算符更改 a.index。所以在a 被销毁时,它会打印1 而不是0。这是可以预料的。 objectWithIncreasedIndex(a) 创建一个索引为 1 的临时文件。该临时文件分配给 a,然后被销毁。
  • “为什么 a 会在这里被破坏?” 它没有。是什么让你相信它确实如此?让构造函数和析构函数打印this 指针的值,而不仅仅是index;还打印&amp;a&amp;b。这可能会很有启发性。您可能还想检测复制/移动构造函数和赋值运算符调用 - 您的程序没有观察到很多事情。

标签: c++ return-value-optimization


【解决方案1】:

当监控构造函数/析构函数时不要忘记复制(/移动)构造函数和赋值,那么你会得到类似的东西:

Init a(0)
- Register Object {0x7ffc74c7ae08: 0}
RVO from a to a
- Register Object {0x7ffc74c7ae0c: 1}
assign {0x7ffc74c7ae08: 0} <- {0x7ffc74c7ae0c: 1}
- Deregister Object {0x7ffc74c7ae0c: 1}
RVO from a to b
- Register Object {0x7ffc74c7ae0c: 2}
End
- Deregister Object {0x7ffc74c7ae0c: 2}
- Deregister Object {0x7ffc74c7ae08: 1}

Demo

RVO 已应用,因为您没有调用复制构造函数。

但分配仍然存在。

a = objectWithIncreasedIndex(a);

等价于

a = RegisteredObject(a.getIndex() + 1);

而不是

a = RegisteredObject(RegisteredObject(a.getIndex() + 1));

感谢 RVO。

对于您的第一个 sn-p,您已经创建了 n(移动)分配和 n 临时对象。

您可以通过使用(旧的)输出变量方式来减少临时变量。

// do the permutation
void squarePermutation(const std::vector<int> &in, std::vector<int>& result)
{
    result.resize(in.size());
    for (int i = 0; i != in.size(); ++i) {
        result[i] = in[in[i]];
    }
}
// Convenient call
std::vector<int> squarePermutation(const std::vector<int> &in)
{
    std::vector<int> result;
    squarePermutation(in, result);
    return result;
}

// do the permutation n times
vector<int> sqarePermutationNTimes(const vector<int> &in, const int n)
{
    std::vector<int> result(in); // Copying here is ok and required
    std::vector<int> tmp; // outside of loop so build only once

    for (int i = 0; i != n; i++)
    {
        squarePermutation(result, tmp);
        std::swap(result, tmp); // Modify internal pointers
    }
    return result;
}

【讨论】:

    猜你喜欢
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2013-02-02
    • 1970-01-01
    • 2021-03-04
    • 1970-01-01
    • 1970-01-01
    相关资源
    最近更新 更多