如何有效克隆动态分配的数组?

时间:2014-12-02 20:30:08

标签: c++

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

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

PoC上的

My attempt最终会出现分段错误:

#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 []'删除它(即视为它被分配了一个简单的'new 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()被调用的方式重写此函数(如果它甚至存在!),同时返回完全相同的结果(让我们假设我们的类型在{{1}中表现良好}和T t; t = other;是等价的),包括可以使用标准T t(other);运算符删除函数结果的事实。

2 个答案:

答案 0 :(得分:3)

  

如何创建一个动态分配的T数组,用std :: uninitialized_copy初始化,以便可以用'delete []'删除它(即视为它被分配了一个简单的'new T [N] “)?

因此,考虑到能够使用delete[]删除内存的相对简单的要求,让我们看看我们有哪些选项。

注意:标准中的所有引用都来自C ++ 14草案N3797,而且我不是标准解释中最好的,所以请稍等一下。

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

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

  

默认行为:

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

避免默认初始化

因此,如果我们想使用new[],我们需要使用delete[], 在 new-expression §5.3.4/ 17 中查看有关初始化的信息标准:

  

创建类型为T的对象的new-expression初始化该对象,如下所示:

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

并转到§8.5/ 7

  

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

     
      
  • 如果T是(可能是cv限定的)类类型(第9节),则调用T的默认构造函数(12.1)(如果T没有默认构造函数或重载解析(13.3),则初始化是错误的)导致歧义或在初始化的上下文中删除或无法访问的函数中;;
  •   
  • 如果T是数组类型,则每个元素都是默认初始化的;
  •   

我们看到如果我们在new[]中省略 new-initializer ,那么数组的所有元素都将通过默认的构造函数进行默认初始化。

那么,如果我们包含 new-initializer ,我们有什么选择吗?回到§5.3.2/ 1

中的定义
  

新初始化:

     
      
  • (表达式列表选项)
  •   
  • 支撑-INIT列表
  •   

我们留下的唯一可能性是 braced-init-list 表达式列表用于非数组 new-expressions )。我设法让它适用于具有编译时间大小的对象,但显然这并不是非常有用。供参考(代码部分改编自here):

#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[]

中的不同类型

总结一下,如果我们想要使用new[],我们不仅需要使用delete[],而且当省略 new-initializer 时,我们的对象也会默认初始化。那么,如果我们使用基本类型分配内存(类似地,在某种程度上,使用placement new),它将使内存保持未初始化,对吧?是的,但是如果它被分配了不同的类型,则未定义用T* ptr = /* whatever */; delete[] ptr之类的内容删除内存。见§5.3.5/ 2

  

在第二个备选(删除数组)中,delete操作数的值可以是空指针值或由前一个数组new-expression产生的指针值。如果不是,则行为未定义。 [注意:这意味着delete-expression的语法必须与new分配的对象的类型匹配,而不是new-expression的语法。 - 结束说明]

§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,但我不建议。

答案 1 :(得分:0)

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

例如:

typedef sdt::vector<A<SomeType> > AVector;

AVector copy(AVector in)
{
   AVector copyArray = in;
   return copyArray;
}