数组的unique_ptr有什么用吗?


238

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

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

但是需要吗?也许使用std::vector或更方便std::array

您发现该构造有什么用吗?


6
为了完整起见,我应该指出,没有C std::shared_ptr<T[]>,但应该存在,如果有人愿意写提案,那么C ++ 14中应该存在。同时,总有boost::shared_array
别名

13
std::shared_ptr<T []>现在在c ++ 17中。
陈力

您可以找到多种在计算机上执行任何操作的方法。这种构造确实有用,尤其是在热路径中,因为如果您确切地知道如何将数组作为目标,它将消除容器操作的开销。另外,它使字符数组毫无疑问地是连续存储的。
kevr

Answers:


256

有些人甚至没有使用std::vector分配器,也不享受使用的奢侈。有些人需要一个动态大小的数组,所以std::array就不用了。有些人从其他已知返回数组的代码中获取数组。而且该代码不会被重写以返回a vector或其他内容。

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

简而言之,您unique_ptr<T[]>需要时使用。当替代品根本无法为您服务时。这是不得已的工具。


27
@NoSenseEtAl:我不确定“某些人不允许这样做”的哪一部分是您所不知道的。一些项目有非常具体的要求,其中可能包括“您不使用vector”。您可以争论这些条件是否合理,但您不能否认它们的存在
Nicol Bolas

21
世界上没有任何原因可以使std::vector人们无法使用std::unique_ptr
Miles Rout 2014年

66
这是不使用vector的原因:sizeof(std :: vector <char>)== 24; sizeof(std :: unique_ptr <char []>)== 8
Arvid 2014年

13
@DanNissenbaum这些项目存在。在一些受到严格审查的行业(例如航空或国防)中,标准库已被禁止使用,因为很难验证和证明它对任何制定法规的理事机构都是正确的。您可能会争辩说标准库已经过充分测试,我会同意您的意见,但是您和我没有制定规则。
艾米丽·L.

16
@DanNissenbaum另外,某些硬实时系统也根本不允许使用动态内存分配,因为系统调用引起的延迟在理论上可能不受限制,并且您无法证明程序的实时行为。否则边界可能太大,从而破坏了WCET限制。尽管这里不适用,因为它们都不使用unique_ptr,但是确实存在那些类型的项目。
艾米丽·L.

124

需要权衡取舍,然后选择与所需内容匹配的解决方案。从我的头顶上:

初始尺寸

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

调整大小

  • array并且unique_ptr<T[]>不允许调整大小
  • vector 确实

存储

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

复制中

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

交换/移动

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

指针/引用/迭代器无效

  • array 确保即使对象处于活动状态,指针,引用和迭代器也不会失效 swap()
  • unique_ptr<T[]>没有迭代器;指针和引用仅swap()在对象处于活动状态时才失效。(交换之后,指针指向交换对象所在的数组,因此在这种情况下它们仍然是“有效的”。)
  • vector 可能会使任何重新分配上的指针,引用和迭代器无效(并提供一些保证,即重新分配只能在某些操作上进行)。

与概念和算法的兼容性

  • array而且vector都是容器
  • unique_ptr<T[]> 不是容器

我必须承认,这似乎是基于策略设计进行某些重构的机会。


1
我不确定我是否理解指针无效的含义。这是关于对象本身的指针还是元素的指针?或者是其他东西?从数组中得到的保证与从向量中得到的保证无关?
jogojapan

3
假设您有一个迭代器,一个指针或对一个元素的引用vector。然后,您要增加大小或容量,以vector强制重新分配。然后,该迭代器,指针或引用不再指向的元素vector。这就是我们所说的“无效”。不会发生此问题array,因为没有“重新分配”。实际上,我只是注意到了一个细节,并对其进行了编辑以适合需要。
别名

1
好的,不会由于数组中的重新分配或unique_ptr<T[]>没有重新分配而导致无效。但是,当然,当数组超出范围时,指向特定元素的指针仍将无效。
jogojapan

是的,如果该对象不再存在,则所有投注均无效。
别名

1
@rubenvb当然可以,但是不能(例如)直接使用基于范围的for循环。顺便说一句,与正常值不同T[],大小(或等效信息)必须挂在某个地方operator delete[]才能正确破坏数组的元素。如果程序员可以访问它,那就太好了。
别名

73

您可能使用a的原因之一unique_ptr是,如果您不想支付对数组进行值初始化的运行时成本。

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-但new不会做,如果T是一个POD。

请参见C ++ 11中的值初始化对象和std :: vector构造函数

请注意,这vector::reserve不是替代方法:在std :: vector :: reserve之后访问原始指针是否安全?

这是同样的原因,C程序员可能选择malloccalloc



@Ruslan在链接的解决方案中,动态数组的元素仍然是值初始化的,但是值初始化不执行任何操作。我会同意,没有意识到没有代码可以实现1000000次不执行任何操作的优化器不值一毛钱,但人们可能宁愿完全不依赖于此优化。
Marc van Leeuwen

还有另一种可能性是提供std::vector一种自定义分配器,该分配器避免构造are类型std::is_trivially_default_constructible和破坏对象are std::is_trivially_destructible,尽管这严格地违反了C ++标准(因为此类类型未默认初始化)。
沃尔特

std::unique_ptr没有提供任何边界检查违背了不少的std::vector实现。
diapir

@diapir与实现无关:std::vector标准要求它检查中的界限.at()。我想您的意思是某些实现也具有可以检入的调试模式.operator[],但是我认为这对于编写良好的可移植代码没有用。
underscore_d

30

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


2
仅仅因为可以复制某些东西并不意味着一定要复制。
Nicol Bolas

4
@NicolBolas:我不明白。出于相同的原因,有人可能会选择使用unique_ptr而不是来阻止这种情况shared_ptr。我想念什么吗?
Andy Prowl

4
unique_ptr不仅仅是防止意外滥用。与相比,它更小且开销更低shared_ptr。关键是,尽管在类中包含防止“滥用”的语义是很好的,但这并不是使用特定类型的唯一原因。并且,vector作为数组存储unique_ptr<T[]>,如果没有其他理由,它的大小要大得多。
Nicol Bolas

3
我以为我阐明了这一点:使用其他特定类型还有其他原因。就像我们有理由喜欢vectorunique_ptr<T[]>如有可能,而不是只是说,“你不能复制”,因此挑选unique_ptr<T[]>时,你不想拷贝。阻止某人做错事不一定是上课的最重要原因。
Nicol Bolas

8
std::vector比a有更多的开销std::unique_ptr-它使用〜3指针而不是〜1。 std::unique_ptr阻止复制构造,但启用移动构造,如果从语义上讲,您正在使用的数据只能被移动而不能被复制,则会感染class包含该数据的数据。对无效数据进行操作实际上会使您的容器类变得更糟,并且“只是不使用它”并不能消除所有的过失。必须将您的每个实例std::vector放到您手动禁用的类中,move这令人头疼。std::unique_ptr<std::array>有一个size
Yakk-Adam Nevraumont

22

Scott Meyers在有效的现代C ++中有此说法

的存在std::unique_ptr对数组应该只有智慧感兴趣的是你的,因为std::arraystd::vectorstd::string几乎总是更好的数据结构的选择,要比原阵列。关于唯一可以std::unique_ptr<T[]>理解的情况是,当您使用类似C的API时,该API返回指向您拥有所有权的堆数组的原始指针。

我认为,查尔斯·索尔维亚(Charles Salvia)的回答很重要:这std::unique_ptr<T[]>是初始化一个空数组的唯一方法,该数组的大小在编译时未知。Scott Meyers对于这种使用动机会说些什么std::unique_ptr<T[]>


4
听起来他只是根本没有设想一些用例,即大小固定但在编译时未知的缓冲区,和/或我们不允许复制的缓冲区。比起vector stackoverflow.com/a/24852984/2436175,它还是一个效率更高的可能原因。
安东尼奥

17

std::vector和相反std::arraystd::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());
}

10

我曾经unique_ptr<char[]>实现过用于游戏引擎中的预分配内存池。这个想法是提供预分配的内存池,而不是动态分配,用于返回冲突请求结果和其他类似粒子物理的东西,而不必在每个帧上分配/释放内存。对于这种情况,您需要内存池来分配寿命有限(通常为一帧,两帧或三帧)的对象,而这些对象不需要销毁逻辑(只需分配内存),这非常方便。


9

某些 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...
...

您可以std::vector<char>在这些情况下使用。
亚瑟塔卡

@ArthurTacca-...如果您不介意编译器将缓冲区中的每个字符一一初始化为0。
TED

9

我遇到了不得不使用std::unique_ptr<bool[]>HDF5库(用于高效二进制数据存储的库,在科学上使用了很多)的情况。某些编译器(在我的情况下std::vector<bool> Visual Studio 2015)提供了压缩(通过在每个字节中使用8个布尔值),这对于诸如HDF5之类的东西来说是灾难性的,它并不关心这种压缩。使用std::vector<bool>,HDF5最终由于该压缩而读取垃圾。

猜猜谁在那儿进行救援,以防万一,std::vector但是我需要干净地分配一个动态数组?:-)


9

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

A std::string带有一个指针,一个长度和一个“短字符串优化”缓冲区。但是我的情况是我需要以几乎成千上万的结构存储几乎总是空的字符串。在C语言中,我只会使用char *,并且大多数时候它都为null。它也适用于C ++,除了a char *没有析构函数,并且不知道删除自身。相反,std::unique_ptr<char[]>当a 超出范围时将删除自身。空值std::string占用32个字节,但空值std::unique_ptr<char[]>占用8个字节,即其​​指针的大小。

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


3

为了回答人们认为您“必须”使用的问题,vector而不是unique_ptr在Device中分配内存时必须在GPU上进行CUDA编程的情况,您必须使用指针数组(带有cudaMalloc)。然后,在主机中检索此数据时,必须再次使用指针,并且unique_ptr可以轻松处理指针。转换double*为的额外成本vector<double>是不必要的,并会导致性能损失。


3

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<...>,则将尝试使用不完整类型的结果,这是编译时错误。


对于这种特殊的用例,我会选择使用Pimpl模式来打破依赖关系-如果仅私下使用它,则可以将定义推迟到实现类方法之前;如果它是公开使用的,则该类的用户应该已经具有有关的具体知识class ALargeAndComplicatedClassWithLotsOfDependencies。因此,从逻辑上讲,您不应遇到此类情况。

3

我不能完全不同意接受的答案的精神。“最后的手段”?离得很远!

我的看法是,与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<std::array>也应该考虑,也不是万不得已的工具。试想一下哪种算法最适合您的算法。

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

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


2

当您只能通过现有的API(认为是窗口消息或与线程相关的回调参数)来戳单个指针时,它们可能是最正确的答案。但与调用代码无关:

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

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

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


2

unique_ptr<char[]>可以在想要C的性能和C ++的便利性的地方使用。考虑您需要对数百万个字符串(如果您还不信任,则为数十亿个)进行运算。在一个单独的存储它们中的每stringvector<char>物体将是存储器(堆)管理例程灾难。特别是如果您需要多次分配和删除不同的字符串。

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

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


一个不把它们全部合而为一vector<char>吗?我想答案是因为创建缓冲区时它们将被初始化为零,而如果使用则不会unique_ptr<char[]>。但是,您的答案中缺少这个关键要素。
亚瑟·塔卡

2
  • 出于二进制兼容性的原因,您需要结构仅包含一个指针。
  • 您需要连接一个API,该API返回分配有 new[]
  • 您的公司或项目有禁止使用的一般规则std::vector,例如,防止粗心的程序员意外引入副本
  • 您要防止粗心的程序员在这种情况下意外引入副本。

有一个普遍的规则,那就是C ++容器比带指针的滚动容器更可取。这是一条一般规则;它有例外。还有更多; 这些只是示例。


0

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

By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.