如何高效地克隆动态分配的阵列

How can I efficiently clone a dynamically allocated array?

本文关键字:动态分配 阵列 何高效 高效      更新时间:2023-10-16

我有一个类,它是一个模板化的智能指针,用于包装动态分配的数组。我知道 STL 中有一些类可以用于此目的,尤其是在 C++11 中,但这是一个广泛使用的内部类。

我希望为它编写一个 Clone(( 方法。我最初的实现使用了 std::copy,但我意识到在分配数组时我应该能够避免默认构造。

我对 PoC 的尝试最终出现分段错误:

#include <iostream>
#include <algorithm>
class A
{
public:
    A(int j) : i(j) {}
    ~A() {std::cout << "Destroying " << i << std::endl;}
private:
int i;
};
int main()
{
    int a[] = {1, 2, 3};
    A* arr = static_cast<A*>(::operator new[](sizeof(A) * 3));
    std::uninitialized_copy(a, a + 3, arr);
    delete [] arr;//::operator delete[](arr);
}

如何创建一个动态分配的 T 数组,使用 std::uninitialized_copy 初始化,以便可以使用"delete []"将其删除(即被视为使用简单的"新 T[N]"分配(?


由于人们似乎难以理解我在问什么,这是我问题的本质:

#include <algorithm>
template <typename T>
T* CloneArray(T* in_array, size_t in_count)
{
    if (!in_array)
        return nullptr;
    T* copy = new T[in_count];
    try
    {
        std::copy(in_array, in_array + in_count, copy);
    }
    catch (...)
    {
        delete [] copy;
        throw;
    }
    return copy;
}

我将如何重写此函数以防止调用T::T()(如果它甚至存在!(,同时返回完全相同的结果(假设我们的类型在该T t; t = other;中表现良好,并且T t(other);是等效的(,包括可以使用标准delete []运算符删除函数的结果。

如何创建一个动态分配的 T 数组,使用 std::uninitialized_copy 初始化,以便可以使用"delete []"将其删除(即被视为使用简单的"新 T[N]"分配(?

因此,鉴于相对简单的要求,即 内存可以删除 delete[] ,让我们看看我们有什么选择。

注意:标准中的所有引用都来自 C++14 草案 N3797,我不是最擅长标准解释的人,所以请持保留态度。

混合malloc()/free()new[]/delete[]

未定义,因为 new 不一定调用 malloc,请参阅 §18.6.1/4(operator new 的默认行为(:

默认行为:

  • 执行循环:在循环中,函数首先尝试分配请求的存储。未指定尝试是否涉及对标准 C 库函数 malloc 的调用。

避免默认初始化

所以,看到我们想使用new[] delete[] ,查看有关新表达式§5.3.4/17 中初始化的信息的标准:

创建类型 T 的对象的新表达式按如下方式初始化该对象:

  • 如果省略 new-initializer,则对象默认初始化 (8.5(;如果未执行初始化,则对象具有不确定的值。
  • 否则,将根据 8.5 的初始化规则解释 new-initializer 以进行直接初始化。

并转到§8.5/7

默认初始化 T 类型的对象意味着:

  • 如果 T 是(可能符合 cv 条件的(类类型(条款 9(,则调用 T 的默认构造函数 (12.1((如果 T 没有默认构造函数或重载解析 (13.3( 导致歧义或函数从初始化上下文中删除或无法访问,则初始化格式不正确(;
  • 如果 T 是数组类型,则每个元素都是默认初始化的;

我们看到,如果我们在new[]中省略一个新初始值设定项,数组的所有元素都将通过其默认构造函数进行默认初始化。

那么,如果我们包含一个新的初始值设定项,我们有什么选择吗?回到 §5.3.2/1 中的定义:

新初始值设定项:

  • (表达式列表选项(
  • 大括号初始化列表

我们剩下的唯一可能性是大括号的初始化列表(表达式列表适用于非数组新表达式(。我设法让它适用于具有编译时大小的对象,但显然这不是很有帮助。供参考(部分代码改编自此处(:

#include <iostream>
#include <utility>
struct A
{
    int id;
    A(int i) : id(i) {
        std::cout << "c[" << id << "]t";}
    A() : A(0) {}
    ~A() {std::cout << "d[" << id << "]t";}
};
template<class T, std::size_t ...I>
T* template_copy_impl(T* a, std::index_sequence<I...>) {
    return new T[sizeof...(I)]{std::move(a[I])...};
}
template<class T, std::size_t N,
    typename Indices = std::make_index_sequence<N>>
T* template_copy(T* a) {
    return template_copy_impl<T>(a, Indices());
}
int main()
{
    const std::size_t N = 3;
    A* orig = new A[N];
    std::cout << std::endl;
    // modify original so we can see whats going on
    for (int i = 0; i < N; ++i)
        orig[i].id = 1 + i;
    A* copy = template_copy<A, N>(orig);
    for (int i = 0; i < N; ++i)
        copy[i].id *= 10;
    delete[] orig;
    std::cout << std::endl;
    delete[] copy;
    std::cout << std::endl;
}

当使用 -std=c++1y(或等效(编译时,应输出如下内容:

c[0]    c[0]    c[0]
d[3]    d[2]    d[1]
d[30]   d[20]   d[10]

new[]delete[]的不同类型

总而言之,如果我们想使用 delete[],我们不仅需要使用 new[],而且在省略 new-initializer 时,我们的对象是默认初始化的。那么,如果我们使用基本类型分配内存(在某种程度上类似于使用放置 new(,它将使内存保持未初始化状态,对吗?是的,但是如果内存分配了不同的类型,则未定义删除内存,例如T* ptr = /* whatever */; delete[] ptr。参见 §5.3.5/2

在第二种备选方案(删除数组(中,删除的操作数的值可以是空指针值,也可以是由上一个数组新表达式产生的指针值。否则,行为未定义。[注意:这意味着删除表达式的语法必须与new分配的对象类型匹配,而不是与new表达式的语法匹配。

§5.3.5/3,它暗示了同样的事情:

在第二种备选方案(删除数组(中,如果要删除的对象的动态类型与其静态类型不同,则行为未定义。

其他选择?

好吧,您仍然可以按照其他人的建议使用unique_ptr。尽管考虑到您被困在大型代码库中,但这可能不可行。再次作为参考,这是我的谦虚实现可能的样子:

#include <iostream>
#include <memory>
struct A
{
    int id;
    A(int i) : id(i) {
        std::cout << "c[" << id << "]t";}
    A() : A(0) {}
    ~A() {std::cout << "d[" << id << "]t";}
};
template<class T>
struct deleter
{
    const bool wrapped;
    std::size_t size;
    deleter() :
        wrapped(true), size(0) {}
    explicit deleter(std::size_t uninit_mem_size) :
        wrapped(false), size(uninit_mem_size) {}
    void operator()(T* ptr)
    {
        if (wrapped)
            delete[] ptr;
        else if (ptr)
        {
            // backwards to emulate destruction order in
            // normally allocated arrays
            for (std::size_t i = size; i > 0; --i)
                ptr[i - 1].~T();
            std::return_temporary_buffer<T>(ptr);
        }
    }
};
// to make it easier on ourselves
template<class T>
using unique_wrap = std::unique_ptr<T[], deleter<T>>;
template<class T>
unique_wrap<T> wrap_buffer(T* orig)
{
    return unique_wrap<T>(orig);
}
template<class T>
unique_wrap<T> copy_buffer(T* orig, std::size_t orig_size)
{
    // get uninitialized memory
    auto mem_pair = std::get_temporary_buffer<T>(orig_size);
    // get_temporary_buffer can return less than what we ask for
    if (mem_pair.second < orig_size)
    {
        std::return_temporary_buffer(mem_pair.first);
        throw std::bad_alloc();
    }
    // create a unique ptr with ownership of our memory, making sure to pass
    // the size of the uninitialized memory to the deleter
    unique_wrap<T> a_copy(mem_pair.first, deleter<T>(orig_size));
    // perform the actual copy and return the unique_ptr
    std::uninitialized_copy_n(orig, orig_size, a_copy.get());
    return a_copy;
}

int main()
{
    const std::size_t N = 3;
    A* orig = new A[N];
    std::cout << std::endl;
    // modify original so we can see whats going on
    for (int i = 0; i < N; ++i)
        orig[i].id = 1 + i;
    unique_wrap<A> orig_wrap = wrap_buffer(orig);
    {
        unique_wrap<A> copy = copy_buffer(orig, N);
        for (int i = 0; i < N; ++i)
            copy[i].id *= 10;
        // if we are passing the original back we can just release it
        A* back_to_somewhere = orig_wrap.release();
        delete[] back_to_somewhere;
        std::cout << std::endl;
    }
    std::cout << std::endl;
}

哪个应该输出:

c[0]    c[0]    c[0]
d[3]    d[2]    d[1]
d[30]   d[20]   d[10]

最后,您也许可以覆盖全局或类operator new/delete,但我不建议这样做。

由于 A 已经是指向包装动态分配内存的类的智能指针,因此可以保证在释放所有引用之前不会释放内存。因此,您可以使用简单的数组或向量并复制智能指针,而无需动态分配数组。

例如:

typedef sdt::vector<A<SomeType> > AVector;
AVector copy(AVector in)
{
   AVector copyArray = in;
   return copyArray;
}