Як передати аргумент унікального_ptr конструктору чи функції?


400

Я новачок, щоб перемістити семантику в C ++ 11, і я не знаю дуже добре, як обробляти unique_ptrпараметри в конструкторах або функціях. Розглянемо цей клас із посиланням на себе:

#include <memory>

class Base
{
  public:

    typedef unique_ptr<Base> UPtr;

    Base(){}
    Base(Base::UPtr n):next(std::move(n)){}

    virtual ~Base(){}

    void setNext(Base::UPtr n)
    {
      next = std::move(n);
    }

  protected :

    Base::UPtr next;

};

Чи так я повинен писати функції, беручи unique_ptrаргументи?

І чи потрібно мені користуватися std::moveв коді виклику?

Base::UPtr b1;
Base::UPtr b2(new Base());

b1->setNext(b2); //should I write b1->setNext(std::move(b2)); instead?


1
Це не помилка сегментації, коли ви викликаєте b1-> setNext на порожньому вказівнику?
балки

Відповіді:


836

Ось можливі способи прийняти унікальний покажчик як аргумент, а також пов'язане з ними значення.

(A) За значенням

Base(std::unique_ptr<Base> n)
  : next(std::move(n)) {}

Для того, щоб користувач зателефонував до цього, він повинен виконати одну з наступних дій:

Base newBase(std::move(nextBase));
Base fromTemp(std::unique_ptr<Base>(new Base(...));

Взяти унікальний покажчик за значенням означає, що ви переносите право власності на покажчик на відповідну функцію / об'єкт / тощо. Після того, newBaseяк побудовано, nextBaseгарантовано буде порожнім . Ви не є власником об'єкта, і навіть більше не маєте вказівника на нього. Це пішло.

Це забезпечується тим, що ми беремо параметр за значенням. std::moveнасправді нічого не рухає ; це просто фантазійний акторський склад. std::move(nextBase)повертає a, Base&&що є посиланням на r-значення nextBase. Це все, що робить.

Оскільки Base::Base(std::unique_ptr<Base> n)бере аргумент за значенням, а не за значенням r-значення, C ++ автоматично побудує тимчасовий для нас. Він створює a std::unique_ptr<Base>з того, Base&&що ми дали функцію via std::move(nextBase). Саме побудова цього тимчасового фактично переміщує значення з nextBaseаргументу функції n.

(B) За допомогою нестандартного посилання на значення l

Base(std::unique_ptr<Base> &n)
  : next(std::move(n)) {}

Це потрібно викликати за фактичним l-значенням (названою змінною). Його не можна назвати тимчасовим, як це:

Base newBase(std::unique_ptr<Base>(new Base)); //Illegal in this case.

Сенс цього поняття такий самий, як і значення будь-якого іншого використання посилань, які не мають const: функція може вимагати або не вимагати права власності на покажчик. Враховуючи цей код:

Base newBase(nextBase);

Немає гарантії, що nextBaseвона порожня. Він може бути порожнім; це не може. Це дійсно залежить від того, що Base::Base(std::unique_ptr<Base> &n)хоче робити. Через це не просто видно з підпису функції, що буде; ви повинні прочитати реалізацію (або пов'язану з нею документацію).

Через це я б не запропонував це як інтерфейс.

(C) За посиланням на значення lst

Base(std::unique_ptr<Base> const &n);

Я не показую реалізацію, тому що ви не можете перейти з а const&. Проходячи a const&, ви говорите, що функція може отримати доступ Baseчерез вказівник, але вона не може зберігати її ніде. Він не може претендувати на право власності на нього.

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

(D) За посиланням на значення r

Base(std::unique_ptr<Base> &&n)
  : next(std::move(n)) {}

Це більш-менш ідентично випадку "по нерейтинговій посиланню на значення". Відмінності - це дві речі.

  1. Ви можете пройти тимчасове:

    Base newBase(std::unique_ptr<Base>(new Base)); //legal now..
  2. Ви повинні використовувати std::moveпри передачі тимчасових аргументів.

Останнє насправді проблема. Якщо ви бачите цей рядок:

Base newBase(std::move(nextBase));

Ви обґрунтовано сподіваєтесь, що після завершення цього рядка він nextBaseповинен бути порожнім. Це слід було перенести з. Зрештою, ви std::moveтам сидите, говорячи, що рух відбувся.

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

Рекомендації

  • (A) За значенням: Якщо ви хочете, щоб функція вимагала права власності на unique_ptr, візьміть його за значенням.
  • (C) За посиланням const l-значення: Якщо ви маєте на увазі для функції просто використовувати функцію "" unique_ptrпротягом тривалості виконання цієї функції, виконайте її const&. Крім того, передайте &або const&вказаний фактичний тип, а не використовуйте unique_ptr.
  • (D) За посиланням r-значення: Якщо функція може або не може вимагати права власності (залежно від внутрішніх шляхів коду), то прийміть її &&. Але я настійно раджу не робити цього, коли це можливо.

Як маніпулювати унікальним_ptr

Ви не можете скопіювати unique_ptr. Ви можете лише перемістити його. Правильний спосіб зробити це за допомогою std::moveстандартної функції бібліотеки.

Якщо ви берете unique_ptrза значенням, ви можете вільно переміщатися від нього. Але рух насправді не відбувається через std::move. Візьміть таке твердження:

std::unique_ptr<Base> newPtr(std::move(oldPtr));

Це дійсно два твердження:

std::unique_ptr<Base> &&temporary = std::move(oldPtr);
std::unique_ptr<Base> newPtr(temporary);

(Примітка. Вищенаведений код технічно не компілюється, оскільки не тимчасові посилання r-значення насправді не є r-значеннями. Це тут лише для демонстраційних цілей).

The temporary лише посилання на значення r oldPtr. Саме в конструкторі з newPtrяких рух відбувається. unique_ptrКонструктор переміщення (конструктор, який бере &&на себе) - це те, що робить власне рух.

Якщо у вас є unique_ptrцінність і ви хочете її десь зберігати, ви повинні використовувати її std::moveдля зберігання.


5
@Nicol: але std::moveне називає його поверненого значення. Пам'ятайте, що названі посилання rvalue - це значення. ideone.com/VlEM3
Р. Мартіньо Фернандес

31
Я в основному погоджуюся з цією відповіддю, але маю деякі зауваження. (1) Я не думаю, що існує правильний випадок використання для передачі посилань на const lvalue: все, що викликає заклик, може зробити з посиланням на const (голий) вказівник, або ще краще, сам покажчик [і це ніхто з його бізнесу, щоб знати, що право власності проводиться через unique_ptr; можливо, деякі інші абоненти потребують такої ж функціональності, але shared_ptrзамість цього проводять виклик lvalue (2) може бути корисним, якщо функція, що викликається, змінює вказівник, наприклад, додаючи або видаляючи (пов'язані зі списком) вузли із пов'язаного списку.
Марк ван Левен

8
... (3) Хоча ваш аргумент на користь передачі значення за значенням над проходженням посилання на rvalue має сенс, я думаю, що сам стандарт завжди передає unique_ptrзначення за допомогою посилання rvalue (наприклад, при їх перетворенні в shared_ptr). Обґрунтуванням цього може бути те, що він дещо ефективніший (перехід до тимчасових покажчиків не робиться), тоді як він дає точно такі ж права абоненту (може передавати rvalues ​​або lvalues, вкладені std::move, але не голі lvalues).
Марк ван Левен

19
Просто повторити те, що сказав Марк, і цитуючи Саттера : "Не використовуйте const unique_ptr & як параметр; замість цього використовуйте віджет *",
Jon

17
Ми виявили проблему з вартістю - переміщення відбувається під час ініціалізації аргументів, що не має упорядкованості стосовно інших оцінок аргументів (крім, звичайно, у списку ініціалізаторів_). Тоді як прийняття посилання rvalue настійно наказує переміщенню відбутися після виклику функції, а отже, після оцінки інших аргументів. Отже, приймаючи посилання на оцінку rvalue, слід віддавати перевагу, коли буде взято право власності.
Бен Войгт

57

Дозвольте спробувати констатувати різні життєздатні режими передачі покажчиків на об’єкти, пам’яттю яких керує екземпляр std::unique_ptrшаблону класу; він також застосовується до std::auto_ptrшаблону старшого класу (який, на мій погляд, дозволяє використовувати всі ті унікальні покажчики, але для яких додатково можуть змінюватися значення, де очікуються rvalues, не потребуючи посилань std::move), а також до певної міри також і до std::shared_ptr.

Як конкретний приклад для обговорення я розгляну наступний простий тип списку

struct node;
typedef std::unique_ptr<node> list;
struct node { int entry; list next; }

Примірники такого списку (яким не можна дозволити ділитися частинами з іншими екземплярами або бути круговими) повністю належать тому, хто має початковий listпокажчик. Якщо код клієнта знає, що список, який він зберігає, ніколи не буде порожнім, він також може вибрати безпосередньо для зберігання першого, nodeа не list. Жодного деструктора не nodeпотрібно визначати: оскільки деструктори для його полів викликаються автоматично, весь список буде рекурсивно видалено деструктором інтелектуального вказівника, коли закінчується термін дії початкового вказівника чи вузла.

Цей рекурсивний тип дає можливість обговорити деякі випадки, які менш помітні у випадку розумного вказівника на звичайні дані. Також самі функції час від часу (рекурсивно) також надають приклад клієнтського коду. Звичайно, typedef для listмає тенденцію до зміни unique_ptr, але це визначення може бути змінено на використання auto_ptrабо shared_ptrзамість цього без особливих змін до того, що сказано нижче (особливо щодо забезпечення безпеки виключень без необхідності писати деструктори).

Режими передачі розумних покажчиків навколо

Режим 0: передайте покажчик або опорний аргумент замість розумного вказівника

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

Наприклад, функція обчислення довжини такого списку повинна бути не listаргументом, а необробленим покажчиком:

size_t length(const node* p)
{ size_t l=0; for ( ; p!=nullptr; p=p->next.get()) ++l; return l; }

Клієнт, який містить змінну, list headможе викликати цю функцію як length(head.get()), тоді як клієнт, який обрав замість цього зберігати node nпредставлений не порожній список, може зателефонувати length(&n).

Якщо вказівник гарантовано не є нульовим (що тут не так, оскільки списки можуть бути порожніми), можна скористатись передавачем, а не вказівником. Це може бути вказівник / посилання на non- constякщо функція потребує оновлення вмісту вузлів (-ів), не додаючи або видаляючи жодного з них (останній передбачає право власності).

Цікавий випадок, який потрапляє в категорію режиму 0, - це (глибока) копія списку; хоча ця функція повинна, звичайно, передати право власності на створену ним копію, вона не стосується права власності на список, який він копіює. Отже, це можна визначити так:

list copy(const node* p)
{ return list( p==nullptr ? nullptr : new node{p->entry,copy(p->next.get())} ); }

Цей код заслуговує уважного огляду, як на питання про те, чому він взагалі компілюється (результат рекурсивного виклику copyв списку ініціалізатора пов'язується з контрольним аргументом rvalue в конструкторі переміщення unique_ptr<node>, aka list, при ініціалізації nextполя згенеровано node), а також на питання, чому це безпечно для винятків (якщо під час процесу рекурсивного розподілу вичерпується пам'ять та якийсь виклик newкидків std::bad_alloc, тоді в цей час вказівник на частково сконструйований список утримується анонімно тимчасовим типом listстворений для списку ініціалізаторів, і його деструктор очистить цей частковий список). До речі, слід протистояти спокусі замінити (як я спочатку робив) другоюnullptr поp, зрештою, як відомо, в цей момент є нульовим: не можна побудувати розумний вказівник від (сировинного) покажчика до константи , навіть коли це, як відомо, є нульовим.

Режим 1: передайте інтелектуальний покажчик за значенням

Функція, яка приймає значення інтелектуального вказівника як аргумент, заволодіває об'єктом, вказаним відразу: інтелектуальний покажчик, який утримує абонент (чи в іменованій змінній, або в анонімній тимчасовій), скопіюється у значення аргументу на вході функції та в телефоні вказівник став нульовим (у випадку тимчасової копії, можливо, було б упущено, але в будь-якому випадку абонент втратив доступ до вказаного об’єкта). Я хотів би зателефонувати цьому режиму дзвінка готівкою : абонент оплачує передню послугу, яку викликає, і не може мати ілюзій щодо власності після дзвінка. Щоб це було зрозуміло, мовні правила вимагають від абонента завершити аргументstd::moveякщо розумний покажчик утримується в змінній (технічно, якщо аргумент є значенням); у цьому випадку (але не для режиму 3 нижче) ця функція робить те, що пропонує її назва, а саме переміщує значення зі змінної на тимчасову, залишаючи змінну нульовою.

У випадках, коли викликана функція безумовно приймає право власності на (піліфери) на об'єкт із загостреним об'єктом, цей режим використовується з std::unique_ptrабо std::auto_ptrє хорошим способом передачі покажчика разом із його власністю, що дозволяє уникнути будь-якого ризику витоку пам'яті. Тим не менш, я думаю, що є лише дуже мало ситуацій, коли режим 3 нижче не слід віддавати перевагу (ніколи не дуже) над режимом 1. З цієї причини я не наводжу прикладів використання цього режиму. (Але дивіться reversedприклад режиму 3 нижче, де зазначається, що режим 1 буде робити як мінімум також.) Якщо функція бере більше аргументів, ніж лише цей покажчик, може статися, що є додатково технічна причина уникнути режиму 1std::unique_ptrабо std::auto_ptr): оскільки фактична операція переміщення відбувається під час передачі змінної вказівникаpза виразом std::move(p)не можна припустити, що він pмістить корисне значення під час оцінки інших аргументів (порядок оцінки не визначений), що може призвести до тонких помилок; навпаки, використання режиму 3 гарантує, що pдо виклику функції не відбувається переміщення , тому інші аргументи можуть безпечно отримати доступ до значення через p.

У режимі використання std::shared_ptrцей режим цікавий тим, що за допомогою єдиного визначення функції він дозволяє абоненту вибирати, чи зберігати спільну копію вказівника для себе, створюючи нову копію спільного використання, яку використовуватиме функція (це відбувається, коли значення lvalue надається аргумент; конструктор копіювання для загальних покажчиків, використовуваних під час виклику, збільшує кількість посилань) або просто надає функції копію вказівника, не зберігаючи жодного і не торкаючись відліку посилань (це відбувається, коли надається аргумент rvalue, можливо lvalue, загорнутий у дзвінок std::move). Наприклад

void f(std::shared_ptr<X> x) // call by shared cash
{ container.insert(std::move(x)); } // store shared pointer in container

void client()
{ std::shared_ptr<X> p = std::make_shared<X>(args);
  f(p); // lvalue argument; store pointer in container but keep a copy
  f(std::make_shared<X>(args)); // prvalue argument; fresh pointer is just stored away
  f(std::move(p)); // xvalue argument; p is transferred to container and left null
}

Те ж можна досягти, окремо визначивши void f(const std::shared_ptr<X>& x)(для випадку lvalue) та void f(std::shared_ptr<X>&& x)(для випадку rvalue), при цьому функції функцій відрізняються лише тим, що перша версія викликає семантику копії (використовуючи побудову / призначення копії при використанні x), а друга версія переміщує семантику (написання std::move(x)замість цього, як у прикладі коду). Отже, для загальних покажчиків режим 1 може бути корисним, щоб уникнути деякого дублювання коду.

Режим 2: передайте інтелектуальний покажчик за (модифікованою) посиланням на значення

Тут функція просто вимагає модифікованого посилання на інтелектуальний вказівник, але не дає ніяких вказівок, що з нею буде робити. Я хотів би викликати цей метод дзвінка карткою : абонент забезпечує оплату, вказавши номер кредитної картки. Довідку можна використовувати для отримання права власності на об'єкт із загостреним об'єктом, але це не обов'язково. Цей режим вимагає надання змінного аргументу lvalue, відповідного тому, що бажаний ефект функції може включати залишення корисного значення в змінній аргументу. Абонент із виразом rvalue, який він бажає передати такій функції, змушений буде зберігати його у названій змінній, щоб мати змогу здійснювати виклик, оскільки мова забезпечує лише неявне перетворення у константупосилання lvalue (посилається на тимчасовий) з rvalue. ( В відміну від ситуації , протилежній від перекачується std::move, гіпсі від Y&&до Y&, з Yсмарт - вказівного типу, що не представляється можливим, проте , це перетворення може бути отриманий з допомогою функції шаблону просто , якщо дійсно потрібної, см https://stackoverflow.com/a/24868376 / 1436796 ). У випадку, коли викликана функція має намір безумовно взяти право власності на об'єкт, викрадаючи аргумент, обов'язок надати аргумент значення має подання неправильного сигналу: змінна не матиме корисного значення після виклику. Тому режим 3, який дає однакові можливості всередині нашої функції, але просить абонентів надати рецензію, слід віддати перевагу такому використанню.

Однак існує дійсний випадок використання для режиму 2, а саме функції, які можуть змінювати покажчик або об’єкт, на який вказується у спосіб, що передбачає право власності . Наприклад, функція, яка префіксує вузол до а, listнадає приклад такого використання:

void prepend (int x, list& l) { l = list( new node{ x, std::move(l)} ); }

Зрозуміло, що тут було б небажано змушувати абонентів використовувати std::move, оскільки їх розумний вказівник все ще має чітко визначений і не порожній список після виклику, хоча і інший, ніж раніше.

Знову ж таки цікаво спостерігати, що відбувається, якщо prependвиклик не працює через відсутність вільної пам'яті. Тоді newдзвінок кине std::bad_alloc; на даний момент часу, оскільки жоден не nodeміг бути виділений, напевно, що передана посилання на оцінку (режим 3) з цього моменту ще не може бути розграблена std::move(l), оскільки це було б зроблено для побудови nextполя того, nodeщо не вдалося виділити. Тож оригінальний смарт-покажчик lвсе ще зберігає оригінальний список, коли помилка видалена; цей список буде або належним чином знищений деструктором смарт-покажчика, або у випадку, lякщо він виживе завдяки достатньо ранньому catchзастереженню, він все одно буде містити початковий список.

Це був конструктивний приклад; з намигненням до цього питання можна також навести деструктивніший приклад видалення першого вузла, що містить задане значення, якщо таке є:

void remove_first(int x, list& l)
{ list* p = &l;
  while ((*p).get()!=nullptr and (*p)->entry!=x)
    p = &(*p)->next;
  if ((*p).get()!=nullptr)
    (*p).reset((*p)->next.release()); // or equivalent: *p = std::move((*p)->next); 
}

Знову ж коректність тут досить тонка. Зокрема, у заключному висловленні вказівник, (*p)->nextщо знаходиться у видаленому вузлі, від’єднується (від release, який повертає вказівник, але робить початковий нуль), перш ніж reset (неявно) знищує цей вузол (коли він знищує старе значення, яке утримує p), гарантуючи, що в цей час знищується один і єдиний вузол. (В альтернативній формі, згаданій у коментарі, цей термін буде залишений внутрішнім органам реалізації оператора переміщення-присвоєння std::unique_ptrекземпляра list; стандарт говорить 20.7.1.2.3; 2, що цей оператор повинен діяти "як би виклик reset(u.release())", тому термін повинен бути безпечним і тут.)

Зверніть увагу , що prependі remove_firstне можуть бути викликані клієнтами , які зберігають локальну nodeзмінну для завжди непорожньої списку, і це правильно , тому що реалізація дається не може працювати в таких випадках.

Режим 3: передайте інтелектуальний вказівник по (модифікованому) посиланню rvalue

Це кращий режим, який потрібно використовувати, коли просто переймаєте вказівник. Я хотів би викликати цей метод викликом чеком : абонент повинен прийняти відмову від власності, як би надаючи готівку, підписавши чек, але фактичне вилучення відкладається до тих пір, поки викликана функція насправді не погрунтує покажчик (саме так, як це було б у режимі 2 ). "Підписання чека" конкретно означає, що абоненти повинні std::moveзавернути аргумент (як у режимі 1), якщо це значення (якщо це rvalue, частина "відмовлятися від власності" очевидна і не потребує окремого коду).

Зауважте, що технічно режим 3 поводиться точно як режим 2, тому викликана функція не повинна брати на себе право власності; Однак я б наполягати на тому, що якщо є якась - або невизначеність в відношенні передачі прав власності (в нормальних умовах експлуатації), режим 2 слід віддати перевагу режим 3, так що режим 3 , використовуючи неявно сигнал для абонентів , що вони будуть віддають власність. Можна сказати, що аргумент лише режиму 1, передаючи дійсно, сигналізує про вимушену втрату права власності на абонентів. Але якщо у клієнта є будь-які сумніви щодо намірів викликаної функції, вона повинна знати характеристики виклику функції, що повинно усунути будь-які сумніви.

Напрочуд важко знайти типовий приклад із залученням нашого listтипу, який використовує передачу аргументів у режимі 3. Переміщення списку bдо кінця іншого списку a- типовий приклад; однак a(який виживає і утримує результат операції) краще передавати режим 2:

void append (list& a, list&& b)
{ list* p=&a;
  while ((*p).get()!=nullptr) // find end of list a
    p=&(*p)->next;
  *p = std::move(b); // attach b; the variable b relinquishes ownership here
}

Чистим прикладом передачі аргументу режиму 3 є наступний, який приймає список (та його право власності) та повертає список, що містить однакові вузли у зворотному порядку.

list reversed (list&& l) noexcept // pilfering reversal of list
{ list p(l.release()); // move list into temporary for traversal
  list result(nullptr);
  while (p.get()!=nullptr)
  { // permute: result --> p->next --> p --> (cycle to result)
    result.swap(p->next);
    result.swap(p);
  }
  return result;
}

Ця функція може бути викликана як l = reversed(std::move(l));повернення списку до себе, але перевернутий список також може використовуватися по-різному.

Тут аргумент негайно переноситься на локальну змінну для ефективності (можна було б використовувати параметр lбезпосередньо на місці p, але тоді доступ до нього кожного разу передбачав би додатковий рівень непрямості); отже, різниця при передачі аргументу 1 режиму мінімальна. Фактично, використовуючи цей режим, аргумент міг служити безпосередньо локальною змінною, уникаючи, таким чином, початкового переміщення; це лише екземпляр загального принципу, що якщо аргумент, переданий за посиланням, служить лише для ініціалізації локальної змінної, можна просто так само передати її за значенням і використовувати параметр як локальну змінну.

Використання режиму 3, як видається, підтримується стандартом, про що свідчить той факт, що всі надані бібліотечні функції, які передають право власності на смарт-покажчики, використовуючи режим 3. Особливим переконливим випадком є ​​конструктор std::shared_ptr<T>(auto_ptr<T>&& p). Цей конструктор використовував (in std::tr1), щоб взяти змінену посилання на значення (як і auto_ptr<T>&конструктор копії), і тому він може бути викликаний значенням auto_ptr<T>lvalue, pяк у std::shared_ptr<T> q(p), після чого pбуло скинуто до нуля. Через зміну аргументу від режиму 2 до 3 цей старий код тепер повинен бути переписаний std::shared_ptr<T> q(std::move(p))і продовжувати працювати. Я розумію, що комітету не сподобався режим 2 тут, але вони мали можливість перейти до режиму 1, визначившисьstd::shared_ptr<T>(auto_ptr<T> p)натомість вони могли б переконатися, що старий код працює без змін, оскільки (на відміну від унікальних покажчиків) авто-покажчики можуть бути мовчки відкинуті до значення (сам об’єкт вказівника скидається до нуля в процесі). Мабуть, комітет настільки віддав перевагу режиму 3 над режимом 1, що вони вирішили активно порушувати існуючий код, а не використовувати режим 1 навіть для вже застарілого використання.

Коли віддати перевагу режиму 3 над режимом 1

Режим 1 є ідеально корисним у багатьох випадках і може віддавати перевагу режиму 3 у випадках, коли припущення власності в іншому випадку має форму переміщення інтелектуального вказівника на локальну змінну, як у reversedнаведеному вище прикладі. Однак я бачу дві причини віддати перевагу режиму 3 у більш загальному випадку:

  • Трохи ефективніше пройти посилання, ніж створити тимчасовий та зав'язати старий покажчик (обробка готівки дещо трудомістка); в деяких сценаріях вказівник може бути переданий кілька разів незмінним до іншої функції, перш ніж він насправді розкрадений. Таке проходження, як правило, вимагатиме написання std::move(якщо не використовується режим 2), але зауважте, що це лише акторський склад, який насправді нічого не робить (зокрема, не відсилає), тому він додає нульову вартість.

  • Чи слід вважати, що що-небудь кидає виняток між початком виклику функції та точкою, коли він (або якийсь міститься виклик) насправді переміщує вказаний об'єкт в іншу структуру даних (і цей виняток вже не потрапляє всередину самої функції ), тоді при використанні режиму 1 об'єкт, на який посилається інтелектуальний вказівник, буде знищений до того, як catchпропозиція може обробляти виняток (оскільки параметр функції був знищений під час розмотування стека), але не так, коли використовується режим 3. Останній дає абонент має можливість відновити дані об'єкта в таких випадках (шляхом вилучення виключення). Зауважте, що режим 1 тут не спричиняє витоку пам'яті , але може призвести до незворотної втрати даних для програми, що також може бути небажаним.

Повернення розумного вказівника: завжди за значенням

На закінчення слова про повернення розумного вказівника, імовірно, вказуючи на об’єкт, створений для використання абонентом. Це насправді не випадок, порівнянний з передачею покажчиків на функції, але для повноти я хотів би наполягати на тому, що в таких випадках завжди повертаються за значеннямне використовують std::move у returnвиписці). Ніхто не хоче отримати посилання на вказівник, який, ймовірно, щойно був прикутий.


1
+1 для режиму 0 - передача основного вказівника замість унікального_ptr. Трохи поза темою (оскільки мова йде про передачу унікального_ptr), але це просто і дозволяє уникнути проблем.
Махта

" режим 1 тут не спричиняє витік пам'яті " - це означає, що режим 3 викликає витік пам'яті, що не відповідає дійсності. Незалежно від того unique_ptr, переміщено чи ні, воно все одно добре видалить значення, якщо воно все ще зберігає його при кожному знищенні або повторному використанні.
rustyx

@RustyX: Я не бачу, як ти розумієш це значення, і я ніколи не мав наміру сказати те, що ти думаєш, що я мав на увазі. Я мав на увазі, що як і в іншому випадку використання unique_ptrзапобігає витоку пам'яті (і, таким чином, в певному сенсі виконує її договір), але тут (тобто, використовуючи режим 1), це може спричинити (за конкретних обставин) щось, що може вважатися ще більш шкідливим а саме втрата даних (знищення вказаного значення), на які можна було б уникнути використання режиму 3.
Марк ван Левен

4

Так, ви повинні, якщо ви приймаєте unique_ptrзначення в конструкторі. Виразність - це приємна річ. Оскільки unique_ptrце не можна скопіювати (приватний копіювальний копіювальник), те, що ви написали, повинно створити помилку компілятора.


3

Редагувати: Ця відповідь неправильна, навіть якщо, строго кажучи, код працює. Я залишаю це лише тому, що дискусія під ним занадто корисна. Ця інша відповідь є найкращою відповіддю, що була надана під час останнього редагування цього: Як я можу передати аргумент unique_ptr конструктору чи функції?

Основна ідея ::std::moveполягає в тому, що люди, які передають вам, unique_ptrповинні використовувати його, щоб висловити знання про те, що вони знають, що unique_ptrвони проходять, втрачають право власності.

Це означає, що ви повинні використовувати посилання rvalue на a unique_ptrу своїх методах, а не на unique_ptrсебе. Це все одно не буде працювати, тому що для проходження простого старого unique_ptrпотрібно буде зробити копію, а це явно заборонено в інтерфейсі для unique_ptr. Цікаво, що використання названого посилання на rvalue знову повертає його у значення, тому вам також потрібно використовувати ::std::move всередині своїх методів.

Це означає, що два способи повинні виглядати так:

Base(Base::UPtr &&n) : next(::std::move(n)) {} // Spaces for readability

void setNext(Base::UPtr &&n) { next = ::std::move(n); }

Тоді люди, які використовують методи, роблять це:

Base::UPtr objptr{ new Base; }
Base::UPtr objptr2{ new Base; }
Base fred(::std::move(objptr)); // objptr now loses ownership
fred.setNext(::std::move(objptr2)); // objptr2 now loses ownership

Як бачите, ::std::moveвисловлюється, що вказівник втратить право власності в той момент, коли це найбільш актуально і корисно знати. Якщо це сталося непомітно, люди, які користуються вашим класом, objptrраптом втратять право власності без жодних очевидних причин.


2
Іменовані посилання rvalue - це значення.
Р. Мартіньо Фернандес

ти впевнений, що це так Base fred(::std::move(objptr));і ні Base::UPtr fred(::std::move(objptr));?
codablank1

1
Щоб додати до свого попереднього коментаря: цей код не компілюється. Ще потрібно використовувати std::moveв реалізації як конструктор, так і метод. І навіть коли ви переходите за значенням, абонент повинен все-таки використовувати std::moveдля передачі значень. Основна відмінність полягає в тому, що при проходженні значення цього інтерфейсу буде зрозуміло, що право власності буде втрачено Дивіться коментар Ніколя Боласа до іншої відповіді.
Р. Мартіньо Фернандес

@ codablank1: Так. Я демонструю, як використовувати конструктор та методи в базі, які беруть посилання на оцінку.
всезнайко

@ R.MartinhoFernandes: О, цікаво. Я думаю, це має сенс. Я очікував, що ви помилитесь, але фактичне тестування виявило, що ви правильні. Виправлено зараз.
всезнайко

0
Base(Base::UPtr n):next(std::move(n)) {}

має бути набагато краще як

Base(Base::UPtr&& n):next(std::forward<Base::UPtr>(n)) {}

і

void setNext(Base::UPtr n)

має бути

void setNext(Base::UPtr&& n)

з тим же тілом.

І ... що evtв handle()??


3
Там немає ніякої вигоди у використанні std::forwardтут: Base::UPtr&&це завжди Rvalue контрольний тип, і std::moveпередає його в якості RValue. Це вже передано правильно.
Р. Мартіньо Фернандес

7
Я категорично не згоден. Якщо функція приймає unique_ptrзначення a , ви гарантуєте, що конструктор переміщення викликав нове значення (або просто те, що вам було надано тимчасове). Це гарантує, що unique_ptrзмінна, яку має користувач, тепер порожня . Якщо ви скористаєтесь ним &&замість, він буде очищений лише в тому випадку, якщо ваш код викликає операцію переміщення. По-твоєму, можлива зміна, з якої користувач не повинен був переміщуватися. Що робить користувача користувачем std::moveпідозрілим і заплутаним. Використання std::moveзавжди повинно забезпечити переміщення чогось .
Нікол Болас

@NicolBolas: Ти маєш рацію. Я видалю свою відповідь, тому що вона працює, але ваше спостереження абсолютно правильне.
Омніфаріоз

0

Вгору проголосували відповідь. Я вважаю за краще пройти повз посилання rvalue.

Я розумію, до чого може призвести проблема щодо проходження повного посилання на rvalue. Але поділимо цю проблему на дві сторони:

  • для абонента:

Я повинен написати код Base newBase(std::move(<lvalue>))або Base newBase(<rvalue>).

  • для callee:

Автор бібліотеки повинен гарантувати, що він фактично перемістить унікальний_птр для ініціалізації учасника, якщо він хоче мати право власності.

Це все.

Якщо ви проходите через посилання rvalue, воно буде викликати лише одну інструкцію "move", але якщо пройти за значенням, це дві.

Так, якщо автор бібліотеки не знає з цього приводу, він може не перемістити unique_ptr для ініціалізації члена, але це проблема автора, а не ви. Що б воно не було за значенням або посилання на оцінку, ваш код такий же!

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

Тепер, для вашого питання. Як передати аргумент унікального_ptr конструктору чи функції?

Ви знаєте, що найкращий вибір.

http://scottmeyers.blogspot.com/2014/07/should-move-only-types-ever-be-passed.html

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