【发布时间】: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<...> 更快,所以我这样做了。由于无法直接交换引用,因此我将其拆分为两个函数,并依靠返回值优化来交换引用。
// 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;还打印&a和&b。这可能会很有启发性。您可能还想检测复制/移动构造函数和赋值运算符调用 - 您的程序没有观察到很多事情。
标签: c++ return-value-optimization