Повернення унікальних_ptr з функцій


367

unique_ptr<T>не дозволяє створювати копію, натомість підтримує семантику переміщення. Тим не менш, я можу повернути a unique_ptr<T>з функції і призначити повернене значення змінній.

#include <iostream>
#include <memory>

using namespace std;

unique_ptr<int> foo()
{
  unique_ptr<int> p( new int(10) );

  return p;                   // 1
  //return move( p );         // 2
}

int main()
{
  unique_ptr<int> p = foo();

  cout << *p << endl;
  return 0;
}

Код, описаний вище, складається і працює за призначенням. То як це, що цей рядок 1не викликає конструктор копій і призводить до помилок компілятора? Якщо б мені довелося використовувати рядок 2замість цього, це мало б сенс (також використовується лінія 2працює, але нам цього не потрібно).

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


Гіпотетично, якщо ви впроваджували заводський метод, ви б віддали перевагу 1 або 2 для повернення фабричної продукції? Я припускаю, що це було б найпоширенішим використанням 1, оскільки при належному заводі ви фактично хочете, щоб право власності на сконструйовану річ перейшло до абонента.
Xharlie

7
@Xharlie? Вони обидва передають право власності на unique_ptr. Ціле запитання полягає в тому, що 1 і 2 - це два різні способи досягнення одного і того ж.
Преторіанський

в цьому випадку RVO також відбувається в c ++ 0x, знищення об'єкта unique_ptr буде одноразово, яке виконується після закінчення mainфункції, але не при fooвиході.
ampawd

Відповіді:


218

Чи є якийсь інший пункт у мовній специфікації, який це використовує?

Так, див. 12.8 §34 та §35:

Коли певні критерії виконуються, реалізація дозволена опускати конструкцію копіювання / переміщення об'єкта класу [...] Це елісія операцій копіювання / переміщення, що називається копіюванням elision , дозволено [...] у звіті про повернення у функція з типом повернення класу, коли вираз - це ім'я енергонезалежного автоматичного об'єкта з тим же cv-некваліфікованим типом, що і тип повернення функції [...]

Коли критерії елісії операції копіювання виконуються і об'єкт, який потрібно скопіювати, позначається рівнем значення, спочатку роздільна здатність перевантаження для вибору конструктора для копії виконується так, як ніби об'єкт був позначений рівнем .


Просто хотілося додати ще один пункт, що повернення за значенням має бути вибором за замовчуванням тут, оскільки в найгіршому випадку називається значення у операторі return, тобто без елісій у C ++ 11, C ++ 14 та C ++ 17. як ревальва. Так, наприклад, наступна функція компілюється з -fno-elide-constructorsпрапором

std::unique_ptr<int> get_unique() {
  auto ptr = std::unique_ptr<int>{new int{2}}; // <- 1
  return ptr; // <- 2, moved into the to be returned unique_ptr
}

...

auto int_uptr = get_unique(); // <- 3

З встановленим прапором на компіляції в цій функції відбуваються два ходи (1 і 2), а потім - наступний хід (3).


@juanchopanza Ви, по суті, маєте на увазі, що foo()насправді також збирається знищити (якби не було призначено нічого), як і значення, що повертається в межах функції, і, отже, має сенс, що C ++ використовує конструктор переміщення при виконанні unique_ptr<int> p = foo();?
корів

1
У цій відповіді йдеться про те, що реалізація дозволена щось робити ... вона не говорить, що це повинно, тому якщо це був єдиний відповідний розділ, це означатиме, що покладатися на таку поведінку не є портативним. Але я не думаю, що це правильно. Я схильний вважати, що правильна відповідь має більше спільного з конструктором руху, як описано у відповіді Нікола Сміляніч та Бартоша Мілевського.
Дон Хетч

6
@DonHatch В ньому йдеться про те, що в цих випадках "дозволено" виконувати копіювання / переміщення elision, але ми не говоримо про копіювання elision тут. Тут застосовується другий цитований абзац, який відповідає за правилами копіювання елісей, але не копіює сам елісій. У другому пункті немає невизначеності - він повністю портативний.
Джозеф Менсфілд

@juanchopanza Я розумію, що це зараз на 2 роки, але ти все ще вважаєш, що це неправильно? Як я вже згадував у попередньому коментарі, мова не йде про копіювання elision. Так буває, що у випадках, коли може бути застосована елізія копіювання (навіть якщо вона не може застосовуватися з std::unique_ptr), існує спеціальне правило спочатку ставитися до об'єктів як до значень. Я думаю, що це повністю узгоджується з тим, що відповів Микола.
Джозеф Менсфілд

1
То чому я все-таки отримую помилку "намагання посилатися на видалену функцію" для мого типу "лише переміщення" (вилучений конструктор копій) при поверненні його точно так само, як у цьому прикладі?
DrumM

104

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

Якби у вас була функція, яка приймає std::unique_ptrяк аргумент, ви б не змогли передати їй p. Вам доведеться явно викликати конструктор переміщення, але в цьому випадку ви не повинні використовувати змінну p після виклику до bar().

void bar(std::unique_ptr<int> p)
{
    // ...
}

int main()
{
    unique_ptr<int> p = foo();
    bar(p); // error, can't implicitly invoke move constructor on lvalue
    bar(std::move(p)); // OK but don't use p afterwards
    return 0;
}

3
@Fred - ну не дуже. Хоча pце не є тимчасовим, результат того foo(), що повертається, є; Таким чином, він є ревальваційним і може бути переміщений, що робить призначення mainможливим. Я б сказав, що ви помилялися, за винятком того, що тоді Ніколас, здається, застосовує це правило до pсебе, яке є помилково.
Едвард Странд

Саме те, що я хотів сказати, але не зміг знайти цих слів. Я видалив цю частину відповіді, оскільки це було не дуже зрозуміло.
Микола Сміляніч

У мене питання: в оригінальному запитанні чи є істотна різниця між лінією 1та лінією 2? На мій погляд , це те ж саме , тому що при побудові pв main, він тільки дбає про тип повертається типу foo, вірно?
Hongxu Chen,

1
@HongxuChen У цьому прикладі абсолютно немає різниці, дивіться цитату зі стандарту у прийнятій відповіді.
Микола Сміляніч

Насправді ви можете використовувати p згодом, доки не призначите його. До цього часу ви не можете спробувати посилатися на вміст.
Алан

38

У унікального_ptr немає традиційного конструктора копій. Натомість у нього є "конструктор переміщення", який використовує посилання rvalue:

unique_ptr::unique_ptr(unique_ptr && src);

Посилання на рецензію (подвійний амперсанд) прив'язуватиметься до рейтингу. Ось чому ви отримуєте помилку під час спроби передати lvalue unique_ptr функції. З іншого боку, значення, яке повертається з функції, трактується як значення, тому конструктор переміщення викликається автоматично.

До речі, це спрацює правильно:

bar(unique_ptr<int>(new int(44));

Тимчасовий jedinstveний_птр тут - рецензія.


8
Думаю, справа в тому, чому можна p- "очевидно" значення " трактувати" як оцінку в зворотному викладі return p;у визначенні foo. Я не думаю, що виникає проблема з тим, що повернене значення самої функції можна "перемістити".
CB Bailey

Чи означає загортання повернутого значення з функції в std :: move означає, що воно буде переміщене двічі?

3
@RodrigoSalazar std :: move - це лише фантастичний перехід від посилання lvalue (&) до посилання на rvalue (&&). Стороннє використання std :: переміщення по рецензуванню посилання просто буде noop
TiMoch

13

Я думаю, що це ідеально пояснено в пункті 25 « Ефективний сучасний C ++» Скотта Майєрса . Ось уривок:

Частина Стандарту, що благословляє RVO, продовжує говорити, що якщо умови для RVO дотримані, але компілятори вирішили не виконувати копіювання елісії, об'єкт, що повертається, повинен розглядатися як реальне значення. Насправді, Стандарт вимагає, що коли дозволено RVO, відбувається або копіювання елісії, або std::moveзастосовано неявно до повернених локальних об'єктів.

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

У вашому випадку код, очевидно, є кандидатом на RVO, оскільки він повертає локальний об'єкт, pа тип такого pж типу, що повертається, що призводить до копіювання елісії. І якщо компілятор вирішить не ухилятись від копії, то з будь-якої причини запустив std::moveби рядок 1.


5

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

Приклад може бути таким:

class Test
{int i;};
std::unique_ptr<Test> foo1()
{
    std::unique_ptr<Test> res(new Test);
    return res;
}
std::unique_ptr<Test> foo2(std::unique_ptr<Test>&& t)
{
    // return t;  // this will produce an error!
    return std::move(t);
}

//...
auto test1=foo1();
auto test2=foo2(std::unique_ptr<Test>(new Test));

Про це згадується у відповіді fredoverflow - чітко виділений " автоматичний об'єкт". Посилання (включаючи посилання rvalue) не є автоматичним об'єктом.
Toby Speight

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