使用 array 的 only_ptr 有什么用处吗?

例如,std::unique_ptr支持数组:

std::unique_ptr<int[]> p(new int[10]);

但是需要吗? 可能使用 std::vectorstd::array更方便。

你觉得这个结构有用吗?

223422 次浏览

std::vector可以被复制,而unique_ptr<int[]>允许表达数组的唯一所有权。另一方面,std::array要求在编译时确定大小,这在某些情况下可能是不可能的。

有些人无法奢侈地使用std::vector,即使是使用分配器。有些人需要一个动态大小的数组,所以std::array被排除了。有些人从已知返回数组的代码中获取数组;并且该代码不会被重写以返回vector或其他东西。

通过允许unique_ptr<T[]>,你可以满足这些需求。

简而言之,当你需要到时,你使用unique_ptr<T[]>。当其他选择都不适合你的时候。这是最后的手段。

我已经使用unique_ptr<char[]>来实现在游戏引擎中使用的预分配内存池。其思想是提供预分配的内存池,而不是用于返回碰撞请求结果和其他诸如粒子物理的东西,而不必在每一帧分配/释放内存。对于这种需要内存池来分配生命时间有限(通常是1帧、2帧或3帧)的对象的场景来说,它非常方便,而且不需要销毁逻辑(只需要内存释放)。

这里有权衡,您可以选择与您想要的匹配的解决方案。我能想到的是:

初始大小

  • vectorunique_ptr<T[]>允许在运行时指定大小
  • array只允许在编译时指定大小

调整

  • arrayunique_ptr<T[]>不允许调整大小
  • vector确实

存储

  • vectorunique_ptr<T[]>将数据存储在对象之外(通常在堆上)
  • array直接将数据存储在对象中

复制

  • arrayvector允许复制
  • unique_ptr<T[]>不允许复制

交换/移动

  • vectorunique_ptr<T[]>有O(1)次swap和move操作
  • array有O(n)次swap和move操作,其中n是数组中元素的数量

指针/引用/迭代器失效

  • array确保对象存在时指针、引用和迭代器永远不会失效,即使在swap()上也是如此
  • unique_ptr<T[]>没有迭代器;指针和引用只在对象处于活动状态时由swap()使其失效。(交换后,指针指向你交换的数组,所以在这个意义上它们仍然是“有效的”。)
  • vector可以使任何重分配上的指针、引用和迭代器无效(并提供一些保证,重分配只能发生在某些操作上)。

概念和算法的兼容性

  • arrayvector都是容器
  • unique_ptr<T[]>不是容器

我不得不承认,对于基于策略的设计来说,这似乎是一个重构的机会。

当你只能通过一个现有的API(窗口消息或线程相关的回调参数)插入一个指针时,它们可能是正确的答案,这些指针在被“捕捉”到另一边后具有一定的生命周期,但与调用代码无关:

unique_ptr<byte[]> data = get_some_data();


threadpool->post_work([](void* param) { do_a_thing(unique_ptr<byte[]>((byte*)param)); },
data.release());

我们都希望事情对自己有利。c++是其他时候用的。

你可能使用unique_ptr的一个原因是如果你不想支付数组value-initializing的运行时成本。

std::vector<char> vec(1000000); // allocates AND value-initializes 1000000 chars


std::unique_ptr<char[]> p(new char[1000000]); // allocates storage for 1000000 chars


// C++20 version:
auto p = std::make_unique_for_overwrite<char[]>(1000000);

std::vector构造函数和std::vector::resize()将对__abc2进行值初始化——但newstd::make_unique_for_overwrite将默认初始化它们,这对PODs来说意味着什么都不做。

看到c++ 11和std::vector构造函数中的值初始化对象

注意,vector::reserve不是这里的替代:在std::vector::reserve后访问原始指针是安全的吗?

这和C程序员选择malloc而不是calloc的原因是一样的。

一些 Windows Win32 API调用中可以找到一个常见的模式,其中使用std::unique_ptr<T[]>可以派上用场,例如,当调用一些Win32 API(将在该缓冲区中写入一些数据)时,你不确切知道输出缓冲区应该有多大:

// Buffer dynamically allocated by the caller, and filled by some Win32 API function.
// (Allocation will be made inside the 'while' loop below.)
std::unique_ptr<BYTE[]> buffer;


// Buffer length, in bytes.
// Initialize with some initial length that you expect to succeed at the first API call.
UINT32 bufferLength = /* ... */;


LONG returnCode = ERROR_INSUFFICIENT_BUFFER;
while (returnCode == ERROR_INSUFFICIENT_BUFFER)
{
// Allocate buffer of specified length
buffer.reset( BYTE[bufferLength] );
//
// Or, in C++14, could use make_unique() instead, e.g.
//
// buffer = std::make_unique<BYTE[]>(bufferLength);
//


//
// Call some Win32 API.
//
// If the size of the buffer (stored in 'bufferLength') is not big enough,
// the API will return ERROR_INSUFFICIENT_BUFFER, and the required size
// in the [in, out] parameter 'bufferLength'.
// In that case, there will be another try in the next loop iteration
// (with the allocation of a bigger buffer).
//
// Else, we'll exit the while loop body, and there will be either a failure
// different from ERROR_INSUFFICIENT_BUFFER, or the call will be successful
// and the required information will be available in the buffer.
//
returnCode = ::SomeApiCall(inParam1, inParam2, inParam3,
&bufferLength, // size of output buffer
buffer.get(),  // output buffer pointer
&outParam1, &outParam2);
}


if (Failed(returnCode))
{
// Handle failure, or throw exception, etc.
...
}


// All right!
// Do some processing with the returned information...
...
  • 出于二进制兼容性的考虑,您需要结构只包含一个指针。
  • 你需要与一个API接口,返回分配给new[]的内存
  • 例如,你的公司或项目有一个禁止使用std::vector的一般规则,以防止粗心的程序员不小心引入副本
  • 您希望防止粗心的程序员在这种情况下意外地引入副本。

有一个普遍的规则,c++容器比使用指针滚动自己的容器更受欢迎。这是一个普遍规律;它有例外。有更多的;这些只是例子。

Scott Meyers在《Effective Modern c++》中这样说

数组std::unique_ptr的存在应该只对你有智力上的兴趣,因为std::arraystd::vectorstd::string实际上总是比原始数组更好的数据结构选择。我能想到的唯一一种情况是,当你使用类似c的API返回一个原始指针,指向你假定拥有的堆数组时,std::unique_ptr<T[]>才有意义

我认为Charles Salvia的答案是相关的:std::unique_ptr<T[]>是初始化一个在编译时不知道大小的空数组的唯一方法。对于使用std::unique_ptr<T[]>的动机,Scott Meyers会说些什么呢?

std::vectorstd::array相反,std::unique_ptr可以拥有一个NULL指针 这在使用需要数组或NULL:

的C api时非常方便
void legacy_func(const int *array_or_null);


void some_func() {
std::unique_ptr<int[]> ptr;
if (some_condition) {
ptr.reset(new int[10]);
}


legacy_func(ptr.get());
}

简而言之:它是迄今为止最节省内存的。

std::string带有指针、长度和“短字符串优化”缓冲区。但我的情况是,我需要存储一个几乎总是空的字符串,在一个有成千上万个的结构中。在C语言中,我只会使用char *,而且大多数时候它都是空的。这也适用于c++,除了char *没有析构函数,并且不知道删除自己。相比之下,std::unique_ptr<char[]>会在超出作用域时删除自己。空的std::string占用32个字节,而空的std::unique_ptr<char[]>占用8个字节,也就是说,正好是其指针的大小。

最大的缺点是,每次我想知道字符串的长度时,我必须对它调用strlen

unique_ptr<char[]>可以用在你想要C的性能和c++的便利性的地方。假设您需要操作数百万(好吧,如果您还不相信,则需要操作数十亿)字符串。将它们分别存储在一个单独的stringvector<char>对象中对于内存(堆)管理例程来说是一场灾难。特别是当您需要多次分配和删除不同的字符串时。

但是,您可以为存储这么多字符串分配一个缓冲区。你不喜欢char* buffer = (char*)malloc(total_size);的原因很明显(如果不明显,搜索“为什么使用智能ptrs”)。你更喜欢unique_ptr<char[]> buffer(new char[total_size]);

通过类比,同样的性能便利性考虑适用于非-char数据(考虑数百万个向量/矩阵/对象)。

我遇到了一个必须使用HDF5库(用于高效二进制数据存储的库,在科学中使用很多)中的std::unique_ptr<bool[]>的情况。一些编译器(在我的情况下是Visual Studio 2015) 提供std::vector<bool>的压缩(通过在每个字节中使用8个bool),这对于HDF5这样的东西来说是一个灾难,它不关心压缩。对于std::vector<bool>, HDF5最终会因为压缩而读取垃圾。

std::vector不起作用的情况下,我需要干净地分配一个动态数组,猜猜谁在那里进行救援?: -)

如果您需要一个不可复制构造的对象的动态数组,那么可以使用一个指向数组的智能指针。例如,如果您需要一个原子数组怎么办?

为了回答人们认为你“必须”使用vector而不是unique_ptr,我在GPU上的CUDA编程中有一个案例,当你在设备中分配内存时,你必须使用指针数组(使用cudaMalloc)。 然后,当在Host中检索该数据时,必须再次寻找指针,而unique_ptr可以很容易地处理指针。 将double*转换为vector<double>的额外代价是不必要的,并且会导致性能损失

允许和使用std::unique_ptr<T[]>的另一个原因,到目前为止还没有在响应中提到:它允许你向前声明数组元素类型。

当你想最小化头文件中的链式#include语句(以优化构建性能)时,这很有用。

例如:

myclass.h:

class ALargeAndComplicatedClassWithLotsOfDependencies;


class MyClass {
...
private:
std::unique_ptr<ALargeAndComplicatedClassWithLotsOfDependencies[]> m_InternalArray;
};

myclass.cpp:

#include "myclass.h"
#include "ALargeAndComplicatedClassWithLotsOfDependencies.h"


// MyClass implementation goes here

使用上面的代码结构,任何人都可以#include "myclass.h"并使用MyClass,而不必包含MyClass::m_InternalArray所需的内部实现依赖项。

如果将m_InternalArray分别声明为std::array<ALargeAndComplicatedClassWithLotsOfDependencies>std::vector<...>,则结果将尝试使用不完整类型,这是一个编译时错误。

我对公认答案的精神再怎么反对也不为过。“最后的手段”?远非如此!

在我看来,与C语言和其他类似语言相比,c++最强大的特性之一是能够表达约束,以便在编译时检查它们,并防止意外误用。因此,在设计结构时,要问问自己它应该允许哪些操作。应该禁止所有其他用途,最好能够静态地(在编译时)实现这些限制,以免误用导致编译失败。

因此,当需要一个数组时,以下问题的答案指定了它的行为: 1. 它的大小是a)在运行时动态的,还是b)静态的,但只在运行时知道,还是c)静态的,在编译时知道? 2. 数组是否可以分配到堆栈上?< / p >

根据这些答案,我认为这是这种数组的最佳数据结构:

       Dynamic     |   Runtime static   |         Static
Stack std::vector      unique_ptr<T[]>          std::array
Heap  std::vector      unique_ptr<T[]>     unique_ptr<std::array>

是的,我认为unique_ptr<std::array>也应该被考虑,这两个都不是最后的手段。想想什么最适合你的算法。

所有这些都通过指向数据数组(vector.data() / array.data() / uniquePtr.get())的原始指针与普通C api兼容。

附注:除了上述考虑之外,还有一个所有权问题:std::arraystd::vector具有值语义(具有按值复制和传递的本机支持),而unique_ptr<T[]>只能移动(强制单一所有权)。这两种方法在不同的场景中都有用。相反,普通静态数组(int[N])和普通动态数组(new int[10])两者都不提供,因此应该尽可能避免——这在绝大多数情况下应该是可能的。如果这还不够的话,普通动态数组也不提供查询其大小的方法——这为内存损坏和安全漏洞提供了额外的机会。

这是一个穷人的std::dynarray

让我们把std::unique_ptr<T[]>看作一个容器。实际上,由于缺少大小字段,并且不能直接用作容器,它在“参数空间”中占据了一个点。标准库中可用的容器是不被其他合适的容器共享的——即使你添加了Boost也不行。

如果你检查我的广泛可用的类向量容器/连续容器的比较,并寻找与std::unique_ptr相同的特性:

  • 堆上的分配
  • 容量在编译时不固定
  • 在建造后不能改变容量(没有完全清理集装箱)

你会发现没有其他容器提供所有这些,除了std::dynarray;但它实际上不在标准库中——它本应被纳入c++ 14,但最终被拒绝了。

我不仅仅是在猜测。甚至在《SO》中,人们也偶尔这样描述事物;参见@KerrekSB的回答 from 2013 to 这个问题

最好避免在代码中使用new。而不是:

size_t size{ 10 }; // for example.
auto buffer = std::make_unique<char[]>(size);