带有数组的 unique_ptr 有啥用吗?

Posted

技术标签:

【中文标题】带有数组的 unique_ptr 有啥用吗?【英文标题】:Is there any use for unique_ptr with array?带有数组的 unique_ptr 有什么用吗? 【发布时间】:2013-05-23 10:35:57 【问题描述】:

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

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

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

你觉得这个结构有什么用处吗?

【问题讨论】:

为了完整起见,我应该指出没有std::shared_ptr&lt;T[]&gt;,但应该有,并且可能会在 C++14 中,如果有人愿意写一个提案的话。同时,总是有boost::shared_array std::shared_ptr 现在在 c++17 中。 您可以找到多种在计算机上执行任何操作的方法。这种结构确实有用,尤其是在热路径中,因为如果您确切知道如何定位您的阵列,它就会消除容器操作的开销。此外,它使字符数组毫无疑问是连续存储的。 我发现这对于与 C 结构互操作很有用,其中结构的成员决定了它的大小。我希望内存自动释放,但没有合适大小的释放类型,所以我使用了一个 char 数组。 【参考方案1】:

有些人没有使用std::vector 的奢侈,即使使用分配器也是如此。有些人需要一个动态大小的数组,所以std::array 不在了。有些人从已知返回数组的其他代码中获取数组;并且该代码不会被重写以返回 vector 或其他内容。

通过允许unique_ptr&lt;T[]&gt;,您可以满足这些需求。

简而言之,当您需要时使用unique_ptr&lt;T[]&gt;。当替代方案根本不适合您时。这是不得已而为之的工具。

【讨论】:

@NoSenseEtAl:我不确定“有些人不允许这样做”的哪一部分让你不明白。有些项目有非常具体的要求,其中可能是“你不会使用vector”。你可以争论这些是否是合理的要求,但你不能否认它们存在 如果有人可以使用std::unique_ptr,世界上没有理由不能使用std::vector 这里有一个不使用向量的理由:sizeof(std::vector) == 24; sizeof(std::unique_ptr) == 8 @DanNissenbaum 这些项目存在。一些受到非常严格审查的行业,例如航空或国防,标准库是禁止使用的,因为很难验证和证明它对任何管理机构制定的规定是正确的。你可能会争辩说标准库已经过很好的测试,我同意你的观点,但你和我没有制定规则。 @DanNissenbaum 此外,一些硬实时系统根本不允许使用动态内存分配,因为系统调用导致的延迟在理论上可能不受限制,并且您无法证明该程序。或者界限可能太大,这会打破您的 WCET 限制。虽然这里不适用,因为他们也不会使用unique_ptr,但这类项目确实存在。【参考方案2】:

存在权衡,您可以选择与您想要的解决方案相匹配的解决方案。在我脑海中浮现:

初始大小

vectorunique_ptr&lt;T[]&gt; 允许在运行时指定大小 array 只允许在编译时指定大小

调整大小

arrayunique_ptr&lt;T[]&gt; 不允许调整大小 vector

存储

vectorunique_ptr&lt;T[]&gt; 将数据存储在对象之外(通常在堆上) array 将数据直接存储在对象中

复制

arrayvector 允许复制 unique_ptr&lt;T[]&gt;不允许复制

交换/移动

vectorunique_ptr&lt;T[]&gt; 有 O(1) 时间 swap 和移动操作 array 有 O(n) 时间 swap 和移动操作,其中 n 是数组中的元素数

指针/引用/迭代器失效

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

与概念和算法的兼容性

arrayvector 都是容器 unique_ptr&lt;T[]&gt; 不是容器

我不得不承认,这似乎是一个使用基于策略的设计进行重构的机会。

【讨论】:

我不确定我是否理解您在 指针失效 上下文中的意思。这是关于指向对象本身的指针,还是指向元素的指针?或者是其他东西?你从数组中得到了什么样的保证,而不是从向量中得到的保证? 假设您有一个迭代器、一个指针或对vector 元素的引用。然后增加vector 的大小或容量,使其强制重新分配。然后该迭代器、指针或引用不再指向vector 的那个元素。这就是我们所说的“失效”。 array 不会出现这个问题,因为没有“重新分配”。实际上,我只是注意到其中的一个细节,并对其进行了编辑以适应它。 好的,不会因为数组中的重新分配或unique_ptr&lt;T[]&gt; 而导致失效,因为没有重新分配。但是当然,当数组超出范围时,指向特定元素的指针仍然会失效。 @rubenvb 当然可以,但不能(比如说)直接使用基于范围的 for 循环。顺便说一句,与普通的T[] 不同,大小(或等效信息)必须在某个地方徘徊,operator delete[] 才能正确销毁数组的元素。如果程序员可以访问它,那就太好了。 @Aidiakapi C++ 要求如果您delete[] 一个包含析构函数的对象数组,则析构函数会运行。出于这个原因,C++ 运行时已经需要知道以这种方式分配的大多数数组的实际大小。现在,如果数组中的对象没有析构函数(例如基本类型)或什么都不做的析构函数,体面的 C++ 实现会优化析构函数。但是,他们通常不会针对这种情况优化内存分配器。它可能会发生,但不会。所以尺寸信息就在那里。【参考方案3】:

您可能使用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

std::vector 构造函数和 std::vector::resize() 将对 T 进行值初始化 - 但如果 T 是 POD,new 不会这样做。

见Value-Initialized Objects in C++11 and std::vector constructor

请注意,vector::reserve 在这里不是替代品:Is accessing the raw pointer after std::vector::reserve safe?

这与 C 程序员可能会选择 malloc 而不是 calloc 的原因相同。

【讨论】:

但是这个原因是not the only solution。 @Ruslan 在链接解决方案中,动态数组的元素仍然是值初始化的,但值初始化什么也不做。我同意一个没有意识到没有代码可以实现 1000000 次什么都不做的优化器一文不值,但人们可能更愿意完全不依赖这种优化。 还有另一种可能性是向std::vector 提供custom allocator,它可以避免构造std::is_trivially_default_constructible 的类型和销毁std::is_trivially_destructible 的对象,尽管这严格违反了C++ 标准(因为这些类型没有默认初始化)。 另外,std::unique_ptr 不提供与许多 std::vector 实现相反的任何边界检查。 @diapir 这与实现无关:标准要求std::vector 检查.at() 中的边界。我猜你的意思是一些实现有调试模式,也会检查.operator[],但我认为这对于编写好的、可移植的代码是没有用的。【参考方案4】:

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

【讨论】:

某些东西可以被复制并不意味着它必须被复制。 @NicolBolas:我不明白。出于与使用unique_ptr 而不是shared_ptr 相同的原因,人们可能想要阻止这种情况。我错过了什么吗? unique_ptr 不仅仅是防止意外误用。它也比shared_ptr 更小和更低的开销。关键是,虽然在类中具有防止“误用”的语义很好,但这并不是使用特定类型的唯一原因。而且vector 作为数组存储比unique_ptr&lt;T[]&gt; 更有用,除非它有一个大小 我想我已经清楚地表明了这一点:还有其他原因来使用特定类型。就像有理由在可能的情况下更喜欢vector 而不是unique_ptr&lt;T[]&gt;,而不是仅仅说“你不能复制它”,因此当你不想要副本时选择unique_ptr&lt;T[]&gt;。阻止某人做错事不一定是选课的最重要原因。 std::vectorstd::unique_ptr 有更多的开销——它使用~3 个指针而不是~1 个指针。 std::unique_ptr 阻止复制构造但启用移动构造,如果在语义上您正在使用的数据只能移动但不能复制,则会感染包含数据的class。对无效的数据进行操作实际上会使你的容器类变得更糟,并且“不要使用它”并不能洗掉所有的罪过。必须将您的 std::vector 的每个实例放入您手动禁用 move 的类中,这令人头疼。 std::unique_ptr&lt;std::array&gt; 有一个 size【参考方案5】:

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

std::unique_ptr 用于数组的存在应该只是您的智力兴趣,因为std::arraystd::vectorstd::string 实际上总是比原始数组更好的数据结构选择。我能想到的唯一一种情况是std::unique_ptr&lt;T[]&gt; 有意义的是,当您使用类似 C 的 API 时,该 API 返回指向您拥有所有权的堆数组的原始指针。

我认为 Charles Salvia 的回答是相关的:std::unique_ptr&lt;T[]&gt; 是初始化其大小在编译时未知的空数组的唯一方法。对于使用std::unique_ptr&lt;T[]&gt; 的动机,Scott Meyers 有什么看法?

【讨论】:

听起来他根本没有设想一些用例,即大小固定但在编译时未知的缓冲区,和/或我们不允许复制的缓冲区。与vector ***.com/a/24852984/2436175相比,效率也是一个可能的原因。【参考方案6】:

std::vectorstd::array相反,std::unique_ptr可以拥有一个空指针。 这在使用需要数组或 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());

【讨论】:

【参考方案7】:

我不能强烈反对已接受答案的精神。 “最后的工具”?远非如此!

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

因此,当需要一个数组时,以下问题的答案会指定它的行为: 1. 它的大小是 a) 在运行时是动态的,还是 b) 静态的,但只在运行时知道,或者 c) 是静态的并且在编译时知道? 2. 数组能否入栈?

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

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

是的,我认为unique_ptr&lt;std::array&gt; 也应该考虑在内,这也不是万不得已的工具。想想什么最适合你的算法。

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

P。 S. 除了上述考虑之外,还有一种所有权:std::arraystd::vector 具有值语义(原生支持复制和按值传递),而unique_ptr&lt;T[]&gt; 只能移动(强制单一所有权)。两者都可以在不同的情况下有用。相反,纯静态数组 (int[N]) 和纯动态数组 (new int[10]) 两者都不提供,因此应尽可能避免使用——这在绝大多数情况下都是可行的。如果这还不够,普通的动态数组也无法查询它们的大小——内存损坏和安全漏洞的额外机会。

【讨论】:

【参考方案8】:

简而言之:它是迄今为止内存效率最高的。

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

最大的缺点是,每次想知道字符串的长度,都得调用strlen就可以了。

【讨论】:

【参考方案9】:

一个常见的模式可以在some Windows Win32 API 调用中找到,其中std::unique_ptr&lt;T[]&gt; 的使用可以派上用场,例如当您在调用某些 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...
...

【讨论】:

在这些情况下你可以使用std::vector&lt;char&gt; @ArthurTacca - ...如果您不介意编译器将缓冲区中的每个字符逐个初始化为 0。【参考方案10】:

我遇到了一个我必须使用std::unique_ptr&lt;bool[]&gt; 的案例,它位于 HDF5 库(高效二进制数据存储库,在科学中经常使用)。一些编译器(在我的例子中是 Visual Studio 2015)provide compression of std::vector&lt;bool&gt;(在每个字节中使用 8 个布尔值),这对于像 HDF5 这样的不关心压缩的东西来说是一场灾难。使用std::vector&lt;bool&gt;,HDF5 最终会因为压缩而读取垃圾。

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

【讨论】:

【参考方案11】:

我使用unique_ptr&lt;char[]&gt; 来实现游戏引擎中使用的预分配内存池。这个想法是提供预先分配的内存池,而不是动态分配来返回碰撞请求结果和粒子物理等其他东西,而不必在每一帧分配/释放内存。对于需要内存池来分配生命周期有限(通常为 1、2 或 3 帧)且不需要销毁逻辑(仅内存释放)的对象的场景,这非常方便。

【讨论】:

【参考方案12】:

允许和使用std::unique_ptr&lt;T[]&gt; 的另一个原因,到目前为止尚未在回复中提及:它允许您前向声明数组元素类型。

当您想要最小化标头中的链式#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&lt;ALargeAndComplicatedClassWithLotsOfDependencies&gt;std::vector&lt;...&gt; - 结果将尝试使用不完整的类型,这是编译时错误。

【讨论】:

对于这个特殊的用例,我会选择 Pimpl 模式来打破依赖——如果它只用于私有,那么定义可以推迟到实现类方法;如果是公开使用,那么该类的用户应该已经对class ALargeAndComplicatedClassWithLotsOfDependencies有了具体的了解。所以从逻辑上讲,你不应该遇到这种情况。 对我来说,通过 unique_ptr 保存一个/几个/一个内部对象数组(从而公开内部类型的名称)而不是使用典型的 PIMPL 引入一个抽象级别更优雅。所以这个答案很有价值。另一个注意事项:当需要将其与 unique_ptr 一起使用时,如果它不是默认可破坏的,则必须包装其内部类型。【参考方案13】: 出于二进制兼容性的原因,您需要在结构中只包含一个指针。 您需要与返回由new[] 分配的内存的API 交互 您的公司或项目有一条禁止使用std::vector 的一般规则,例如,以防止粗心的程序员意外引入副本 您希望防止粗心的程序员在这种情况下意外引入副本。

一般规则是 C++ 容器优先于使用指针滚动自己的容器。这是一般规则;它有例外。还有更多;这些只是示例。

【讨论】:

【参考方案14】:

要回答那些认为你“必须”使用vector 而不是unique_ptr 的人,我有一个在GPU 上进行CUDA 编程的案例,当您在设备中分配内存时,您必须使用指针数组(使用cudaMalloc)。 然后,在 Host 中检索此数据时,您必须再次获取指针,unique_ptr 可以轻松处理指针。 将double* 转换为vector&lt;double&gt; 的额外成本是不必要的,并且会导致性能损失。

【讨论】:

【参考方案15】:

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

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

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

我们都希望事情对我们有利。 C++ 用于其他时间。

【讨论】:

【参考方案16】:

unique_ptr&lt;char[]&gt; 可以用在你想要 C 的性能和 C++ 的便利性的地方。考虑您需要对数百万(好吧,如果您还不信任的话,数十亿)字符串进行操作。将它们中的每一个存储在单独的stringvector&lt;char&gt; 对象中对于内存(堆)管理例程来说将是一场灾难。特别是如果您需要多次分配和删除不同的字符串。

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

以此类推,同样的性能和便利性考虑适用于非char 数据(考虑数百万个向量/矩阵/对象)。

【讨论】:

一个不把它们都放在一个大vector&lt;char&gt;?我想答案是因为当你创建缓冲区时它们将被零初始化,而如果你使用unique_ptr&lt;char[]&gt;,它们就不会被初始化。但是您的答案中缺少这个关键金块。【参考方案17】:

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

【讨论】:

【参考方案18】:

tl;dr:这是一个穷人的std::dynarray

让我们将std::unique_ptr&lt;T[]&gt; 视为一个容器。虽然它确实因缺少大小字段而被削弱,并且不能直接用作容器,但它在标准库可用的容器的“参数空间”中占据了一个点,该点不被其他任何人共享,适当的,容器 - 即使您将 Boost 添加到组合中。

如果您查看我的comparison of widely-available vector-like/contiguous containers,并寻找与std::unique_ptr 相同的功能:

堆上的分配 编译时容量不固定 构建后无法更改容量(无需完全清除容器)

您会发现,除了std::dynarray 之外,没有其他容器提供所有这些功能;但这实际上不在标准库中——它应该进入 C++14,但最终被拒绝了。

而且我不仅仅是在猜测。即使在 SO 上,这也是偶尔描述事物的方式;参见 @KerrekSB's answer 从 2013 年到 this question。

【讨论】:

以上是关于带有数组的 unique_ptr 有啥用吗?的主要内容,如果未能解决你的问题,请参考以下文章

setNeedsDisplay:NO 有啥用吗?

Ruby:没有块的选择/查找有啥用吗?

这种定义JS对象的方式有啥用吗?

对 const 的右值引用有啥用吗?

default_if_none 在 Django 模板中有啥用吗?

JAVA中.class文件是啥意思?有啥用吗?