C++中不调用默认构造函数创建初始化cuda_ptr类数组的方法
问题本质
- C++原生固定长度数组如果声明时不做显式聚合初始化,会强制要求元素类型提供可访问的默认构造函数,这是之前不得不保留无参默认构造的核心原因。
- 观察到的临时对象析构现象判断是正确的:循环赋值时生成的临时
cuda_ptr对象,在赋值操作完成后会立刻析构。由于当前类没有自定义拷贝/移动赋值逻辑,默认生成的赋值操作只会浅拷贝data指针,临时对象析构时会直接调用cudaFree释放指针,最终数组元素存储的是已经被释放的野指针,会触发double free或者非法访问错误。
实现方案
首先先对cuda_ptr类做必要的RAII合规修改,从根源上禁止非法操作:
- 直接删除默认构造函数,不允许无参创建无效的
cuda_ptr实例 - 删除拷贝构造、拷贝赋值函数,避免同一块显存被多个
cuda_ptr对象持有导致重复释放 - 实现移动构造、移动赋值函数,支持显存所有权的安全转移
修改后的类代码如下:
template <typename T> class cuda_ptr { public: // 禁用默认构造 cuda_ptr() = delete; cuda_ptr(size_t device_number, T* allocdata ) : dev_number(device_number), data(allocdata) { std::cout<<"create pointer"<<std::endl; safeCudaSelectDevice(dev_number); } // 禁用拷贝 cuda_ptr(const cuda_ptr&) = delete; cuda_ptr& operator=(const cuda_ptr&) = delete; // 实现移动语义 cuda_ptr(cuda_ptr&& other) noexcept : dev_number(other.dev_number), data(other.data) { other.data = nullptr; other.dev_number = 0; } cuda_ptr& operator=(cuda_ptr&& other) noexcept { if (this != &other) { // 释放当前持有的显存 if (data != nullptr) { safeCudaSelectDevice(dev_number); cudaFree(data); } // 接管对方的显存所有权 dev_number = other.dev_number; data = other.data; other.data = nullptr; other.dev_number = 0; } return *this; } ~cuda_ptr(){ if (data != nullptr) { safeCudaSelectDevice(dev_number); cudaFree(data); std::cout<<"delete pointer"<<std::endl; } } size_t dev_number; T* data; };
固定长度数组最优方案:std::array配合编译期初始化
std::array和原生数组内存布局完全一致,没有额外性能开销,配合编译期索引序列可以直接对所有元素做显式构造,全程不触发默认构造,也没有临时对象析构问题。
先写两个辅助模板函数,自动按设备序号构造数组元素:
template <typename T, size_t... Is> std::array<cuda_ptr<T>, sizeof...(Is)> make_cuda_ptr_array_impl(size_t elem_bytes, std::index_sequence<Is...>) { return std::array<cuda_ptr<T>, sizeof...(Is)>{ cuda_ptr<T>(Is, safeCudaMalloc<T>(elem_bytes))... }; } template <size_t ArrayLen, typename T> std::array<cuda_ptr<T>, ArrayLen> make_cuda_ptr_array(size_t elem_count) { return make_cuda_ptr_array_impl<T>(elem_count * sizeof(T), std::make_index_sequence<ArrayLen>{}); }
使用方式非常简单,直接声明即可:
// 创建长度为2的cuda_ptr<float>数组,两个元素分别绑定设备0、1,各分配N个float的显存 auto dev_A = make_cuda_ptr_array<2, float>(N); // 访问方式和原生数组完全一致:dev_A[0].data 就是设备0上的显存指针
可变长度数组方案:std::vector配合原位构造
如果数组长度需要运行时确定,使用std::vector提前预留空间后调用emplace_back原位构造元素,同样不会触发默认构造:
std::vector<cuda_ptr<T>> dev_A; // 提前预留足够空间,避免扩容时的移动开销 dev_A.reserve(array_length); for(int i = 0; i < array_length; i++){ // 直接在vector的内存位置构造元素,不生成临时对象 dev_A.emplace_back(i, safeCudaMalloc<T>(N * sizeof(T))); }
注意:不要使用原生数组+placement new的手动内存管理方案,很容易遗漏构造、析构调用,触发难以排查的内存错误,标准库容器已经对这类场景做了完善的封装,没有必要重复实现。
内容的提问来源于stack exchange,提问作者JanJHF
相关产品推荐
相关产品推荐

