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

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 тоже работает, но мы не обязаны это делать).

Я знаю, что С++ 0x разрешает это исключение с unique_ptr, поскольку возвращаемое значение является временным объектом, который будет уничтожен, как только функция выйдет, тем самым гарантируя уникальность возвращаемого указателя. Мне интересно, как это реализовано, является ли оно особенным в компиляторе или есть ли какое-то другое предложение в спецификации языка, которое это использует?

Ответ 1

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

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

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

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


Просто хотелось добавить еще одну точку, которая должна быть выбрана по умолчанию, потому что именованное значение в операторе return в худшем случае, то есть без исключений в С++ 11, С++ 14 и С++ 17 рассматривается как rvalue. Так, например, следующая функция компилируется с флагом -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).

Ответ 2

Это никак не относится к 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

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

unique_ptr::unique_ptr(unique_ptr && src);

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

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

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

Временной unique_ptr здесь является rvalue.

Ответ 4

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

Часть Стандартного благословения RVO продолжает утверждать, что если условия для RVO выполнены, но компиляторы предпочитают не выполнять копирование, возвращаемый объект должен рассматриваться как rvalue. Фактически, Стандарт требует, чтобы при разрешении 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));