我有一个函数,其中二叉"树"的节点填充了基于输入向量递归计算的值,该向量表示叶子上的值。 该函数的旧C++实现如下
using namespace std;
double f(const size_t h, vector<double>& input) {
double** x;
x = new double*[h+1];
x[0] = input.data();
for (size_t l = 1; l <= h; l++)
x[l] = new double[1<<(h-l)];
// do the computations on the tree where x[l][n] is the value
// on the node n at level l.
result = x[l][0];
for (size_t l = 1; l <= h; l++)
delete[] x[l];
delete[] x;
return result;
}
现在,我正在尝试使用 C++11/C++14 中的智能指针编写代码的"现代"实现。 我试图使用数组的专用化来定义x
std::unique_ptr
这样我就不必更改"计算"过程。 这种方法的明显问题是,"input"的内容将在函数结束时被删除(因为获取数据所有权的唯一指针将在函数结束时被销毁)。
一个简单(也许是安全的)解决方案是在x
中为整个树(包括叶子)分配内存,并在函数的开头将叶子的值从input
复制到x[0]
(在这种情况下,我甚至可以使用嵌套的std::vector
s 而不是专门用于数组的std::unique_ptr
s作为x
的类型)。 但我宁愿避免复制的成本。
或者,可以更改计算过程,直接从input
而不是从x
读取叶子的值,这需要更改太多的小代码片段。
还有其他方法可以做到这一点吗?
C++11/14 并没有真正引入任何在使用现代std::vector
来管理动态数组内存之前尚未实现的内容。
[
std::unique_ptr
] 的明显问题是"input"的内容将在函数末尾被删除
事实上。您不得"窃取"输入向量的缓冲区(除非通过交换或移动进入另一个向量)。这将导致未定义的行为。
或者,可以更改计算过程,直接从输入而不是从x读取叶子的值,这需要更改太多小段代码。
这种选择很有意义。目前尚不清楚为什么输入向量必须由x[0]
指向。循环从 1 开始,因此它们似乎没有使用它。如果它只被直接引用,那么使用输入参数本身会更有意义。使用显示的代码,我希望这将大大简化您的函数。
此外,输入不被视为const st::vector&的事实也困扰着我。
这是不指向可修改x[0]
输入向量的另一个原因。但是,可以使用const_cast
来解决此限制。这就是const_cast
所针对的情况。
让我们假设从此以后,输入是本地数组数组的一部分是有意义的。
一个简单(也许是安全的)解决方案是在 x 中为整棵树(包括叶子)分配内存......我什至可以使用嵌套的 std::vectors ...但我宁愿避免复制的成本。
如果使用向量向量,则不一定需要复制。您可以将输入向量交换或移动到x[0]
中。处理完成后,如果需要,您可以通过交换或移回来恢复输入。如果按照建议将输入分开,则这些都不是必需的。
我建议另一种方法。以下建议主要是性能优化,因为它将分配数减少到 2。作为奖励,它恰好也很容易满足您从本地数组数组指向输入向量的愿望。这个想法是将所有树分配到一个平面向量中,并为内容向量中的裸指针分配另一个向量。
下面是一个使用输入向量作为x[0]
的示例,但如果选择直接使用输入,则很容易更改。
double f(const size_t h, const std::vector<double>& input) {
std::vector<double*> x(h + 1);
x[0] = const_cast<double*>(input.data()); // take extra care not to modify x[0]
// (1 << 0) + (1 << 1) + ... + (1 << (h-1)) == (1 << h) - 1
std::vector<double> tree((1 << h) - 1);
for (std::size_t index = 0, l = 1; l <= h; l++) {
x[l] = &tree[index];
index += (1 << (h - l));
}
// do the computations on the tree where x[l][n] is the value
// on the node n at level l.
return x[l][0];
}
这当然看起来像是std::vector<std::vector<double>>
的工作,而不是std::unique_ptr
,但具有额外的复杂性,即您在概念上希望向量仅拥有其内容的一部分,而第一个元素是对输入向量(而不是副本)的非所有者引用。
这不是直接可能的,但您可以添加额外的间接层以实现所需的效果。如果我正确理解您的问题,您希望x
行为,使其支持参数 0 表示input
的operator[]
,而参数 0>表示x
本身拥有的数据。
我会为此编写一个简单的容器,根据std::vector
实现。这是一个玩具示例;我已将容器称为SpecialVector
:
#include <vector>
double f(const std::size_t h, std::vector<double>& input) {
struct SpecialVector {
SpecialVector(std::vector<double>& input) :
owned(),
input(input)
{}
std::vector<std::vector<double>> owned;
std::vector<double>& input;
std::vector<double>& operator[](int index) {
if (index == 0) {
return input;
} else {
return owned[index - 1];
}
}
void add(int size) {
owned.emplace_back(size);
}
};
SpecialVector x(input);
for (std::size_t l = 1; l <= h; l++)
x.add(1<<(h-l));
// do the computations on the tree where x[l][n] is the value
// on the node n at level l.
auto result = x[1][0];
return result;
}
int main() {
std::vector<double> input { 1.0, 2.0, 3.0 };
f(10, input);
}
此方法允许其余的旧代码继续使用[]
,就像以前一样。
编写一个类Row
,其中包含一个用于控制销毁行为的所有权标志并实现operator[]
,然后创建一个行vector
。
如上所述,如果input
是常量,你就会遇到问题,因为你不能在编译器级别显式地强制执行它,你必须小心不要在你不能写的地方写,但这并不比你现在的更糟。
我没有尝试编译它,但您的新Row
类可能看起来有点像这样。
class Row
{
double *p;
bool f;
public:
Row() :p(0), f(false) {}
void init(size_t n) { p = new double[n]; f=true; }
void init(double *d) { p=d;, f=false;}
double operator[](size_t i) { return p[i]; }
~Row() { if (flag) delete[] p; }
};