Есть ли какое-либо использование для unique_ptr с массивом?

238

std::unique_ptr имеет поддержку массивов, например:

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

но нужно ли это? Вероятно, это более удобно для использования std::vectorили std::array.

Нашли ли вы какую-либо пользу для этой конструкции?

топь
источник
6
Для полноты, я должен указать, что нет std::shared_ptr<T[]>, но должно быть, и, вероятно, будет в C ++ 14, если кто-то может быть обеспокоен, чтобы написать предложение. В то же время, всегда есть boost::shared_array.
псевдоним
13
std::shared_ptr<T []> в C ++ 17 сейчас.
力 力
Вы можете найти несколько способов сделать что-нибудь на компьютере. Эта конструкция действительно используется, особенно в горячем пути, потому что она устраняет накладные расходы на контейнерные операции, если вы точно знаете, как нацелить свой массив. Кроме того, он создает массивы символов без каких-либо сомнений в непрерывном хранении.
Кевр

Ответы:

256

Некоторые люди не могут себе позволить пользоваться std::vectorдаже распределителями. Некоторым людям нужен динамически изменяемый массив, поэтому std::arrayего нет. И некоторые люди получают свои массивы из другого кода, который, как известно, возвращает массив; и этот код не будет переписан, чтобы вернуть vectorили что-то.

Позволяя unique_ptr<T[]>, вы обслуживаете эти потребности.

Короче говоря, вы используете, unique_ptr<T[]>когда вам нужно . Когда альтернативы просто не будут работать на вас. Это инструмент последней инстанции.

Николь Болас
источник
27
@ NoSenseEtAl: Я не уверен, какая часть «некоторым людям не разрешено делать это» ускользает от вас. Некоторые проекты имеют очень специфические требования, и среди них могут быть «вы не можете использовать vector». Вы можете утверждать, являются ли это разумными требованиями или нет, но вы не можете отрицать, что они существуют .
Николь Болас
21
В мире нет причин, по которым кто-то не сможет использовать, std::vectorесли сможет std::unique_ptr.
Майлз Рут
66
вот причина не использовать vector: sizeof (std :: vector <char>) == 24; sizeof (std :: unique_ptr <char []>) == 8
Arvid
13
@DanNissenbaum Эти проекты существуют. В некоторых отраслях, которые находятся под пристальным вниманием, например, авиация или оборона, стандартная библиотека закрыта, потому что ее трудно проверить и доказать, что она является правильной по отношению к тому, какой руководящий орган устанавливает правила. Вы можете утверждать, что стандартная библиотека хорошо протестирована, и я согласен с вами, но вы и я не устанавливаем правила.
Эмили Л.
16
@DanNissenbaum Кроме того, некоторые жесткие системы реального времени вообще не могут использовать динамическое выделение памяти, поскольку задержка, вызываемая системным вызовом, может быть теоретически не ограничена, и вы не сможете доказать поведение программы в реальном времени. Или граница может быть слишком большой, что нарушает ваш лимит WCET. Хотя здесь это и не применимо, так как они не будут использоваться, unique_ptrно такие проекты действительно существуют.
Эмили Л.
125

Есть компромиссы, и вы выбираете решение, которое соответствует тому, что вы хотите. С верхней части моей головы:

Начальный размер

  • vectorи unique_ptr<T[]>позвольте размеру быть определенным во время выполнения
  • array только позволяет указать размер во время компиляции

Изменение размера

  • arrayи unique_ptr<T[]>не разрешать изменение размера
  • vector делает

Место хранения

  • vectorи unique_ptr<T[]>хранить данные вне объекта (обычно в куче)
  • array хранит данные непосредственно в объекте

Копирование

  • arrayи vectorразрешить копирование
  • unique_ptr<T[]> не позволяет копировать

Обмен / ход

  • vectorи unique_ptr<T[]>иметь O (1) время swapи операции перемещения
  • arrayимеет O (n) время swapи операции перемещения, где n - количество элементов в массиве

Недействительный указатель / ссылка / итератор

  • array гарантирует, что указатели, ссылки и итераторы никогда не будут признаны недействительными, пока объект активен, даже если swap()
  • unique_ptr<T[]>не имеет итераторов; указатели и ссылки становятся недействительными, только если swap()объект активен. (После замены указатели указывают на массив, с которым вы меняли местами, поэтому они все еще «действительны» в этом смысле.)
  • vector может аннулировать указатели, ссылки и итераторы при любом перераспределении (и предоставляет некоторые гарантии, что перераспределение может произойти только при определенных операциях).

Совместимость с концепциями и алгоритмами

  • arrayи vectorоба контейнера
  • unique_ptr<T[]> это не контейнер

Я должен признать, что это похоже на возможность некоторого рефакторинга с дизайном на основе политик.

Псевдоним
источник
1
Я не уверен, что понимаю, что вы имеете в виду в контексте аннулирования указателя . Это касается указателей на сами объекты или указателей на элементы? Или что-то другое? Какую гарантию вы получаете от массива, который вы не получаете от вектора?
Джогоджапан
3
Предположим, что у вас есть итератор, указатель или ссылка на элемент a vector. Затем вы увеличиваете размер или емкость vectorтакого, что оно вызывает перераспределение. Тогда этот итератор, указатель или ссылка больше не будут указывать на этот элемент vector. Это то, что мы подразумеваем под «недействительностью». С этой проблемой не бывает array, потому что нет «перераспределения». На самом деле, я просто заметил эту деталь и отредактировал ее так, чтобы она подходила.
Псевдоним
1
Хорошо, не может быть недействительности в результате перераспределения в массиве или unique_ptr<T[]>потому, что перераспределения не существует. Но, конечно, когда массив выходит из области видимости, указатели на определенные элементы все равно будут недействительными.
Джогоджапан
Да, все ставки отключены, если объект больше не живой.
псевдоним
1
@rubenvb Конечно, вы можете, но вы не можете (скажем) использовать петли для диапазонов напрямую. Кстати, в отличие от нормального 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.

Смотрите Value-Initialized Objects в C ++ 11 и конструктор std :: vector

Обратите внимание, что vector::reserveздесь нет альтернативы: доступ к необработанному указателю после std :: vector :: reserve безопасен?

Это та же причина , программист C может выбрать mallocболее calloc.

Чарльз Сальвиа
источник
Но эта причина не единственное решение .
Руслан
@Ruslan В связанном решении элементы динамического массива все еще инициализируются значением, но инициализация значения ничего не делает. Я бы согласился с тем, что оптимизатор, который не понимает, что бездействие 1000000 раз может быть реализовано без кода, не стоит ни копейки, но можно предпочесть вообще не зависеть от этой оптимизации.
Марк ван Леувен
еще одна возможность состоит в том, чтобы std::vectorпредоставить пользовательскому распределителю, который избегает создания типов std::is_trivially_default_constructibleи уничтожения объектов std::is_trivially_destructible, хотя это строго нарушает стандарт C ++ (поскольку такие типы не инициализируются по умолчанию).
Уолтер
Также std::unique_ptrне предоставляет никакой связанной проверки вопреки многим std::vectorреализациям.
диапир
@diapir Дело не в реализации: std::vectorСтандарт требует проверки границ .at(). Полагаю, вы имели в виду, что в некоторых реализациях есть режимы отладки, которые .operator[]тоже регистрируются, но я считаю, что это бесполезно для написания хорошего переносимого кода.
underscore_d
30

std::vectorМогут быть скопированы вокруг, в то время как unique_ptr<int[]>позволяет выразить уникальное владение массива. std::arrayс другой стороны, требует, чтобы размер был определен во время компиляции, что может быть невозможно в некоторых ситуациях.

Энди Проул
источник
2
То, что что-то можно скопировать, не означает, что так должно быть.
Николь Болас
4
@NicolBolas: я не понимаю. Можно хотеть предотвратить это по той же причине, по которой можно было бы использовать unique_ptrвместо shared_ptr. Я что-то упускаю?
Энди Prowl
4
unique_ptrделает больше, чем просто предотвращает случайное неправильное использование. Это также меньше и меньше, чем накладные расходы shared_ptr. Дело в том, что хотя в классе и есть семантика, предотвращающая «неправильное использование», это не единственная причина, по которой следует использовать определенный тип. И vectorгораздо более полезен в качестве хранилища массивов, чем unique_ptr<T[]>, если только по той причине, что он имеет размер .
Николь Болас
3
Я думал, что ясно дал понять: есть другие причины для использования определенного типа, чем этот. Так же , как есть причины предпочитать vectorболее , unique_ptr<T[]>где это возможно, вместо того , чтобы просто сказать, «вы не можете копировать» и , следовательно , выбрать , unique_ptr<T[]>когда вы не хотите копии. Удержание кого-либо от неправильных действий не обязательно является самой важной причиной выбора класса.
Николь Болас
8
std::vectorимеет больше накладных расходов, чем std::unique_ptr- он использует ~ 3 указателя вместо ~ 1. std::unique_ptrблокирует построение копирования, но разрешает построение перемещения, которое, если семантически данные, с которыми вы работаете, можно только перемещать, но не копировать, заражает classсодержащие данные. Имея операцию над данными, не действует на самом деле делает ваш класс контейнера хуже, и «просто не использовать его» не смывает все грехи. Необходимость помещать каждый свой экземпляр std::vectorв класс, где вы отключаете вручную, move- головная боль. std::unique_ptr<std::array>имеет size.
Якк - Адам Невраумонт
22

Скотт Мейерс говорит об этом в Effective Modern C ++

Существование std::unique_ptrдля массивов должны быть только интеллектуальный интерес к вам, потому что std::array, std::vector, std::stringпрактически всегда выбор лучше структур данных , чем сырые массивы. Об единственной ситуации, которую я могу себе представить, когда std::unique_ptr<T[]>смысл имеет смысл, когда вы используете C-подобный API, который возвращает необработанный указатель на массив кучи, владельцем которого вы являетесь.

Я думаю, что ответ Чарльза Сальвии важен: это std::unique_ptr<T[]>единственный способ инициализировать пустой массив, размер которого неизвестен во время компиляции. Что бы сказал Скотт Мейерс об этой мотивации для использования std::unique_ptr<T[]>?

newling
источник
4
Похоже, он просто не представлял несколько вариантов использования, а именно буфер, размер которого фиксирован, но неизвестен во время компиляции, и / или буфер, для которого мы не разрешаем копирование. Существует также эффективность в качестве возможной причины, чтобы предпочесть его, чтобы vector stackoverflow.com/a/24852984/2436175 .
Антонио
17

В отличие от std::vectorи std::array, std::unique_ptrможет иметь нулевой указатель.
Это удобно при работе с C API, которые ожидают либо массив, либо NULL:

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[]>для реализации предварительно выделенных пулов памяти, используемых в игровом движке. Идея состоит в том, чтобы предоставить предварительно распределенные пулы памяти, используемые вместо динамических распределений для возврата результатов запросов на столкновения и других вещей, таких как физика элементарных частиц, без необходимости выделять / освобождать память в каждом кадре. Это довольно удобно для таких сценариев, когда вам нужны пулы памяти для выделения объектов с ограниченным сроком службы (обычно один, 2 или 3 кадра), которые не требуют логики уничтожения (только освобождение памяти).

Саймон Феркель
источник
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...
...
Mr.C64
источник
Вы могли бы просто использовать std::vector<char>в этих случаях.
Артур Такка
@ArthurTacca - ... если вы не возражаете против компилятора, инициализирующего каждый символ в вашем буфере до 0 по одному.
TED
9

Я столкнулся со случаем, когда мне пришлось использовать std::unique_ptr<bool[]>, который был в библиотеке HDF5 (библиотека для эффективного хранения двоичных данных, много использованная в науке). Некоторые компиляторы (в моем случае Visual Studio 2015) обеспечивают сжатиеstd::vector<bool> (используя 8 bools в каждом байте), что является катастрофой для чего-то вроде HDF5, который не заботится об этом сжатии. При этом std::vector<bool>HDF5 в конечном итоге считывал мусор из-за этого сжатия.

Угадайте, кто был там для спасения, в случае, когда std::vectorне сработало, и мне нужно было аккуратно выделить динамический массив? :-)

Квантовый физик
источник
9

В двух словах: это наиболее эффективная память.

A std::stringпоставляется с указателем, длиной и буфером «оптимизации коротких строк». Но в моей ситуации мне нужно хранить строку, которая почти всегда пуста, в структуре, которой у меня сотни тысяч. В С я бы просто использовал char *, и большую часть времени он был бы нулевым. Это также работает для C ++, за исключением того, что a char *не имеет деструктора и не знает, как удалить себя. Напротив, a std::unique_ptr<char[]>удалит себя, когда выйдет из области видимости. Пустой std::stringзанимает 32 байта, но пустой std::unique_ptr<char[]>занимает 8 байтов, то есть точно размер его указателя.

Самым большим недостатком является то, что каждый раз, когда я хочу узнать длину строки, мне приходится обращаться strlenк ней.

jorgbrown
источник
3

Чтобы ответить тем, кто думает, что вы «должны» использовать vectorвместо « unique_ptrУ меня есть случай программирования CUDA на GPU», когда вы выделяете память в Device, вы должны использовать массив указателей (с cudaMalloc). Затем, при получении этих данных в Host, вы должны снова пойти за указателем, и unique_ptrвы можете легко справиться с указателем. За дополнительной платой преобразования double*в vector<double>ненужно и приводит к потере перфорации.

Romain Laneuville
источник
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вместо этого было объявлено как a std::array<ALargeAndComplicatedClassWithLotsOfDependencies>или a 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>также следует учитывать, и ни один из них не является последним средством. Подумайте, что лучше всего подходит для вашего алгоритма.

Все они совместимы с простыми API C через необработанный указатель на массив данных ( vector.data()/ array.data()/ uniquePtr.get()).

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());

Мы все хотим, чтобы все было хорошо для нас. С ++ для другого времени.

Саймон Бьюкен
источник
2

unique_ptr<char[]>можно использовать там, где вы хотите производительность C и удобство C ++. Учтите, что вам нужно оперировать миллионами (ну, миллиардами, если вы еще не доверяете) строк. Хранение каждого из них в отдельном объекте stringили vector<char>объекте было бы катастрофой для процедур управления памятью (кучей). Особенно, если вам нужно выделить и удалить разные строки много раз.

Однако вы можете выделить один буфер для хранения такого количества строк. Вам не понравится char* buffer = (char*)malloc(total_size);по понятным причинам (если не очевидно, поиск «зачем использовать умные ptrs»). Вы бы предпочлиunique_ptr<char[]> buffer(new char[total_size]);

По аналогии, те же соображения производительности и удобства применимы к не- charданным (рассмотрим миллионы векторов / матриц / объектов).

Серж Рогач
источник
Один не положил их всех в один большой vector<char>? Ответ, я полагаю, заключается в том, что они будут инициализироваться нулем при создании буфера, а не будут, если вы используете unique_ptr<char[]>. Но этот ключевой самородок отсутствует в вашем ответе.
Артур Такка
2
  • Ваша структура должна содержать только указатель по причинам совместимости с двоичными файлами.
  • Вы должны взаимодействовать с API, который возвращает память, выделенную с new[]
  • У вашей фирмы или проекта есть общее правило std::vector, запрещающее использование , например, предотвращения случайного введения копий неосторожными программистами.
  • Вы хотите, чтобы неосторожные программисты случайно не вводили копии в этом случае.

Существует общее правило, что контейнеры C ++ предпочтительнее, чем использование собственных указателей. Это общее правило; у него есть исключения. Есть еще кое-что; это всего лишь примеры.

Джимми Хартцелл
источник
0

Если вам нужен динамический массив объектов, которые не поддаются копированию, то разумный указатель на массив - это путь. Например, что если вам нужен массив атомиков.

Илья Минкин
источник