Возвращение unique_ptr из функций

367

unique_ptr<T>не позволяет создавать копии, вместо этого он поддерживает семантику перемещения. Тем не менее, я могу вернуть функцию 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, потому что при правильной фабрике вы на самом деле хотите, чтобы владение созданной вещью передавалось вызывающей стороне.
Ксарли,
7
@Xharlie? Они оба передают право собственности unique_ptr. Весь вопрос в том, что 1 и 2 - это два разных способа достижения одного и того же.
преторианец
в этом случае RVO также происходит в c ++ 0x, уничтожение объекта unique_ptr будет происходить один раз, что выполняется после mainвыхода из функции, но не после fooвыхода.
ampawd

Ответы:

219

есть ли еще какое-то предложение в спецификации языка, которое это эксплуатирует?

Да, см. 12.8 §34 и §35:

При соблюдении определенных критериев реализация может опустить конструкцию копирования / перемещения объекта класса [...] Это исключение операций копирования / перемещения, называемое копированием , разрешено [...] в операторе возврата в функция с типом возврата класса, когда выражение является именем энергонезависимого автоматического объекта с тем же типом cv-unqualified, что и тип возврата функции [...]

Когда критерии для исключения операции копирования выполнены и объект, который должен быть скопирован, обозначен lvalue, разрешение перегрузки для выбора конструктора для копии сначала выполняется так, как если бы объект был обозначен rvalue .


Просто хотел добавить еще одно замечание, что возвращение по значению должно быть здесь выбором по умолчанию, потому что именованное значение в операторе возврата в худшем случае, т.е. без пропусков в 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).

fredoverflow
источник
@juanchopanza Вы, по сути, имеете в виду, что foo()оно действительно также должно быть уничтожено (если оно не было назначено чему-либо), точно так же, как возвращаемое значение в функции, и, следовательно, имеет смысл, что C ++ использует конструктор перемещения при выполнении unique_ptr<int> p = foo();?
7
1
Этот ответ говорит, что реализации разрешено что-то делать ... он не говорит, что должен, поэтому, если бы это был единственный релевантный раздел, это означало бы, что полагаться на это поведение нельзя переносимо. Но я не думаю, что это правильно. Я склонен думать, что правильный ответ больше связан с конструктором перемещения, как описано в ответе Николы Смилянича и Бартоша Милевского.
Дон Хэтч
6
@DonHatch В нем говорится, что «разрешено» выполнять копирование / перемещение в этих случаях, но мы не говорим здесь о разрешении копирования. Здесь применяется второй процитированный абзац, который связан с правилами копирования, но не является самим копированием. Во втором абзаце нет никакой неопределенности - он полностью переносим.
Джозеф Мэнсфилд
@juanchopanza Я понимаю, что это сейчас 2 года спустя, но вы все еще чувствуете, что это неправильно? Как я уже упоминал в предыдущем комментарии, речь идет не об исключении копирования. Так уж получилось, что в случаях, когда может применяться 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
@ Фред - ну не совсем. Хотя pэто не временно, результат того foo(), что возвращается, есть; таким образом, это значение и может быть перемещено, что делает назначение mainвозможным. Я бы сказал, что вы были неправы, за исключением того, что Никола, кажется, применяет это правило к pсебе, которое по ошибке.
Эдвард Стрендж
Именно то, что я хотел сказать, но не смог найти слов. Я удалил эту часть ответа, так как это было не очень ясно.
Никола Смильянич
У меня вопрос: в оригинальном вопросе есть ли существенная разница между линией 1и линией 2? На мой взгляд , это то же самое , так как при построении pв main, он только заботится о типе возвращаемого типа foo, верно?
Хунсю Чен
1
@HongxuChen В этом примере нет абсолютно никакой разницы, посмотрите цитату из стандарта в принятом ответе.
Никола Смильянич
На самом деле, вы можете использовать p впоследствии, если вы назначаете ему. До этого вы не можете ссылаться на содержимое.
Алан
38

unique_ptr не имеет традиционного конструктора копирования. Вместо этого у него есть «конструктор перемещения», который использует ссылки rvalue:

unique_ptr::unique_ptr(unique_ptr && src);

Ссылка rvalue (двойной амперсанд) будет связываться только с rvalue. Вот почему вы получаете ошибку, когда пытаетесь передать lvalue unique_ptr в функцию. С другой стороны, значение, возвращаемое функцией, рассматривается как значение r, поэтому конструктор перемещения вызывается автоматически.

Кстати, это будет работать правильно:

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

Временный unique_ptr здесь является значением.

Бартош Милевски
источник
8
Я думаю, что суть в том, почему p«явно» lvalue можно рассматривать как rvalue в выражении return return p;в определении foo. Я не думаю, что есть какая-то проблема с тем фактом, что возвращаемое значение самой функции может быть «перемещено».
CB Bailey
Означает ли перенос возвращаемого значения из функции в std :: move, что оно будет перемещено дважды?
3
@RodrigoSalazar std :: move - это просто причудливый приведение от ссылки lvalue (&) к ссылке rvalue (&&). Постороннее использование std :: move для ссылки на rvalue будет просто преувеличением
TiMoch
13

Я думаю, что это прекрасно объясняется в статье 25 « Эффективного современного С ++» Скотта Мейерса . Вот выдержка:

Далее в части стандарта, благословляющей 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));
v010dya
источник
Упоминается в ответе fredoverflow - четко выделен « автоматический объект». Ссылка (включая ссылку на rvalue) не является автоматическим объектом.
Тоби Спейт
@TobySpeight Хорошо, прости. Я предполагаю, что мой код является только разъяснением.
v010дя