Чи є якесь використання унікального_ptr з масивом?


238

std::unique_ptr має підтримку масивів, наприклад:

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

але це потрібно? можливо, це зручніше використовувати std::vectorабо std::array.

Чи знайшли ви користь для цієї конструкції?


6
Для повноти я мушу зазначити, що немає std::shared_ptr<T[]>, але має бути, і, мабуть, це буде в С ++ 14, якщо хтось може потурбуватися написати пропозицію. Тим часом завжди є boost::shared_array.
Псевдонім

13
std::shared_ptr<T []> зараз знаходиться в c ++ 17.
陳 力

Ви можете знайти кілька способів зробити що-небудь на комп’ютері. Ця конструкція має користь, особливо на гарячому шляху, оскільки вона викорінює накладні операції з контейнерами, якщо ви точно знаєте, як націлити свій масив. Крім того, він створює масиви символів без жодних сумнівів щодо суміжного зберігання.
kevr

Відповіді:


256

Деяким людям немає розкоші в користуванні std::vector, навіть з розподільниками. Деяким людям потрібен масив динамічного розміру, тому std::arrayйого немає. І деякі люди отримують свої масиви з іншого коду, який, як відомо, повертає масив; і цей код не буде переписаний для поверненняvector чи інше.

Дозволяючи unique_ptr<T[]>, ви обслуговуєте ці потреби.

Коротше кажучи, ви використовуєте, unique_ptr<T[]>коли вам потрібно . Коли альтернативи просто не будуть працювати на вас. Це останній засіб.


27
@NoSenseEtAl: Я не впевнений, яка частина "деяких людей не може цього робити" ухиляється від вас. Деякі проекти мають дуже специфічні вимоги, і серед них може бути "ви не отримаєте використання vector". Ви можете сперечатися, чи це розумні вимоги, чи ні, але ви не можете заперечити їх існування .
Нікол Болас

21
У світі немає жодної причини, чому хтось не міг би користуватися, std::vectorякщо вміє користуватися std::unique_ptr.
Майлз Рут

66
ось причина не використовувати вектор: sizeof (std :: vector <char>) == 24; sizeof (std :: unique_ptr <char []>) == 8
Арвід

13
@DanNissenbaum Ці проекти існують. У деяких галузях, що знаходяться під дуже жорстким контролем, наприклад, авіація або оборона, стандартна бібліотека є поза межами, тому що важко перевірити і довести, що це правильно, незалежно від того, яким органом управління встановлює положення. Ви можете стверджувати, що стандартна бібліотека добре перевірена, і я би погодився з вами, але ви і я не дотримуюся правил.
Емілі Л.

16
@DanNissenbaum Також деяким жорстким системам реального часу взагалі заборонено використовувати динамічний розподіл пам'яті, оскільки затримка системного виклику може бути теоретично обмежена, і ви не можете довести поведінку програми в реальному часі. Або обмеження може бути занадто великим, що порушує ваш ліміт WCET. Хоча тут не застосовується, оскільки вони не використовували б unique_ptrжодного, але такі проекти дійсно існують.
Емілі Л.

124

Є компроміси, і ви вибираєте рішення, яке відповідає бажаному. Вгорі голови:

Початковий розмір

  • 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
Я не впевнений, що я розумію, що ви маєте на увазі в контексті недійсності покажчиків . Це стосується покажчиків на самі об’єкти чи вказівників на елементи? Або щось інше? Яку гарантію ви отримуєте з масиву, який ви не отримуєте з вектора?
jogojapan

3
Припустимо, у вас є ітератор, вказівник або посилання на елемент a vector. Тоді ви збільшуєте розмір або місткість vectorтакого, що примушує перерозподіляти. Тоді цей ітератор, вказівник чи посилання більше не вказує на цей елемент vector. Це те, що ми маємо на увазі під "недійсним". З цією проблемою не трапляється array, оскільки немає "перерозподілу". Насправді, я просто помітив деталь із цим, і я відредагував це на манеру.
Псевдонім

1
Гаразд, не може бути визнано недійсним в результаті перерозподілу масиву або unique_ptr<T[]>через те, що немає перерозподілу. Але звичайно, коли масив вийде за межі, покажчики на конкретні елементи все одно будуть недійсними.
jogojapan

Так, усі ставки знімаються, якщо об'єкт більше не працює.
Псевдонім

1
@rubenvb Звичайно, ви можете, але ви не можете (скажімо,) використовувати діапазон на основі діапазону безпосередньо для циклів. Між іншим, на відміну від звичайного T[], розмір (або еквівалентна інформація) повинен десь висіти, operator delete[]щоб правильно знищити елементи масиву. Було б добре, якби програміст мав доступ до цього.
Псевдонім

73

Однією з причин, яку ви можете використовувати, 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::reserveтут не є альтернативою: чи безпечний доступ до необмеженого покажчика після std :: vector :: резерв?

Це та ж причина , програміст 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[], але я вважаю це марним для написання хорошого портативного коду.
підкреслити

30

Копіювати std::vectorможна навколо, при цьому unique_ptr<int[]>дозволяє висловити унікальну власність на масив. std::arrayз іншого боку, вимагає визначати розмір під час компіляції, що може бути неможливим у деяких ситуаціях.


2
Тільки тому, що щось можна скопіювати, не означає, що це повинно бути.
Нікол Болас

4
@NicolBolas: Я не розумію. Можна попередити це з тієї ж причини, чому unique_ptrзамість цього можна було б використовувати shared_ptr. Я щось пропускаю?
Енді Проул

4
unique_ptrробить більше, ніж просто запобігає випадковому використанню. Він також менший і нижчий накладні, ніж shared_ptr. Справа в тому, що, хоча приємно мати семантику в класі, яка запобігає «неправильному використанню», це не єдина причина використовувати певний тип. І vectorнабагато корисніше як сховище масиву, ніж unique_ptr<T[]>, якщо не з іншої причини, крім того, що він має розмір .
Нікол Болас

3
Я подумав, що я зрозумів, що існують інші причини, ніж застосовувати певний тип. Так само , як є причини надавати перевагу vectorбільш , unique_ptr<T[]>де це можливо, замість того , щоб просто сказати, «ви не можете копіювати» і , отже , вибрати , unique_ptr<T[]>коли ви не хочете копії. Зупинення когось робити неправильно - це не обов'язково найважливіша причина вибору класу.
Нікол Болас

8
std::vectorмає більше накладних витрат, ніж a std::unique_ptr- він використовує ~ 3 вказівника замість ~ 1. std::unique_ptrблокує побудову копії, але дозволяє створювати переміщення, яке, якщо дані, з якими ви працюєте, семантично переміщувати, але не копіювати, заражає дані, що classмістять дані. Маючи операцію над даними, не діє на насправді робить ваш клас контейнера гірше, і «просто не використовувати його" не змиває всі гріхи. Необхідно помістити кожен екземпляр свого std::vectorкласу, де ви вручну відключите, move- це головний біль. std::unique_ptr<std::array>має size.
Якк - Адам Невраумон

22

Скотт Меєрс має це сказати в «Ефективних сучасних C ++»

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

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


4
Схоже, він просто не передбачив декілька випадків використання, а саме буфер, розмір якого фіксований, але невідомий під час компіляції, та / або буфер, для якого ми не дозволяємо копіювати. Також ефективність є можливою причиною віддавати перевагу vector стакковерф'ю.com/ a/ 24852984/2436175 .
Антоніо

17

Всупереч std::vectorі std::array, std::unique_ptrможе володіти вказівником NULL.
Це стане в нагоді при роботі з API 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

Загальну схему можна знайти в деяких викликах API Windows Win32 , в яких використання std::unique_ptr<T[]>може стати в нагоді, наприклад, коли ви точно не знаєте, яким має бути великий вихідний буфер при виклику якогось API Win32 (який запише деякі дані всередині що буфер):

// 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 один на один.
ТЕД

9

Я зіткнувся з випадком, коли мені довелося користуватися std::unique_ptr<bool[]>, який знаходився в бібліотеці HDF5 (Бібліотека для ефективного зберігання бінарних даних, багато використовувалася в науці). Деякі компілятори (у моєму випадку Visual Studio 2015) забезпечують стисненняstd::vector<bool> (використовуючи 8 булів у кожному байті), що є катастрофою для чогось типу HDF5, що не хвилює цього стиснення. З std::vector<bool>, HDF5 зрештою читав сміття через це стиснення.

Здогадайтесь, хто там був для порятунку, якщо справа std::vectorне працювала, і мені потрібно було чітко виділити динамічний масив? :-)


9

Коротше кажучи: це далеко не найефективніша пам'ять.

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

Найбільшим недоліком є ​​те, що кожного разу, коли я хочу знати довжину струни, мені доводиться дзвонити strlenна неї.


3

Щоб відповісти людям, які думають, що ви «повинні» використовувати vectorзамість unique_ptrмене, у мене є випадок програмування CUDA на графічному процесорі, коли ви виділяєте пам’ять в «Пристрій», ви повинні перейти до масиву вказівників (з cudaMalloc). Потім, отримуючи ці дані в Host, потрібно знову перейти за вказівником і 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замість цього було оголошено як a std::array<ALargeAndComplicatedClassWithLotsOfDependencies>, або a std::vector<...>, відповідно - результатом буде спроба використання незавершеного типу, що є помилкою часу компіляції.


Для цього конкретного випадку використання я б обрав для схеми Pimpl розрив залежності - якщо він використовується лише приватно, то визначення можна відкласти, поки не будуть застосовані методи класу; якщо він використовується публічно, то користувачі класу повинні були вже мати конкретні знання про class ALargeAndComplicatedClassWithLotsOfDependencies. Отже, логічно, вам не слід натрапляти на такі сценарії.

3

Я не можу досить сильно погодитися з духом прийнятої відповіді. "Інструмент останньої інстанції"? Далеко від цього!

Як я бачу, однією з найсильніших особливостей C ++ порівняно з C та деякими іншими подібними мовами є здатність виражати обмеження, щоб їх можна було перевірити під час компіляції та запобігти випадковому використанню. Тому проектуючи структуру, запитайте себе, які операції вона повинна дозволяти. Усі інші використання повинні бути заборонені, і найкраще, якщо такі обмеження можуть бути застосовані статично (під час компіляції), щоб неправильне використання призвело до помилки компіляції.

Отже, коли потрібен масив, відповіді на наступні запитання визначають його поведінку: 1. Чи є його розмір а) динамічним під час виконання, або б) статичним, але відомим лише під час виконання, або с) статичним і відомим під час компіляції? 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 API через необроблений вказівник на масив даних ( 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());

Ми всі хочемо, щоб речі були приємними для нас. C ++ - це для інших часів.


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

Якщо вам потрібен динамічний масив об'єктів, які не можуть бути сконфігуровані копією, то розумний вказівник на масив - це шлях. Наприклад, що робити, якщо вам потрібен масив атомів.

Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.