Возвращает элемент unique_ptr из метода класса

#c #c 11 #smart-pointers #rvo

#c #c 11 #интеллектуальные указатели #rvo

Вопрос:

Я пытаюсь вернуть std::unique_ptr class элемент (пытаясь передать право собственности) вызывающему. Ниже приведен пример фрагмента кода:

 class A {
public:
  A() : p {new int{10}} {}

  static std::unique_ptr<int> Foo(A amp;a) {
    return a.p; // ERROR: Copy constructor getting invoked
                // return std::move(a.p); WORKS FINE
  }

  std::unique_ptr<int> p;
};
  

Я думал, что компилятор (gcc-5.2.1) сможет выполнить оптимизацию возвращаемого значения (исключение копирования) в этом случае, не требуя явного намерения через std::move() . Но это не так. Почему бы и нет?

Следующий код, похоже, работает нормально, что кажется эквивалентным:

 std::unique_ptr<int> foo() {
  std::unique_ptr<int> p {new int{10}};
  return p;
}
  

Комментарии:

1. Это отличный первый вопрос. Добро пожаловать в StackOverflow!

Ответ №1:

Правило в [class.copy] является:

[…] когда выражение в return операторе является (возможно, заключенным в скобки) выражением идентификатора, которое именует объект с автоматической продолжительностью хранения, объявленной в теле или предложении объявления параметра самой внутренней функции или лямбда-выражения, разрешение перегрузки для выбора конструктора для копии сначала выполняется, как если быобъект был обозначен значением rvalue .

В этом примере:

 std::unique_ptr<int> foo() {
  std::unique_ptr<int> p {new int{10}};
  return p;
}
  

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

Но в этом примере:

 static std::unique_ptr<int> Foo(A amp;a) {
    return a.p;
}
  

это не применяется. a.p это вообще не имя объекта, поэтому мы не пытаемся разрешить перегрузку, как если бы это было значение rvalue , вместо этого мы просто делаем обычную вещь: пытаемся скопировать его. Это не удается, поэтому вы должны явно move() указать это.


Это формулировка правила, но оно может не ответить на ваш вопрос. Почему это правило? В принципе — мы пытаемся быть в безопасности. Если мы присваиваем имя локальной переменной, всегда безопасно перейти от нее к оператору return . К нему больше никогда не будет доступа. Простая оптимизация, никаких возможных недостатков. Но в вашем исходном примере a эта функция не принадлежит ни этой функции, ни a.p ей . Переход от него по своей сути небезопасен, поэтому язык не будет пытаться сделать это автоматически.

Ответ №2:

Исключение копирования не может быть применено (среди других причин), поскольку a.p является a std::unique_ptr , которое невозможно скопировать. И поскольку a.p имеет время жизни за пределами тела A::Foo(Aamp;) , было бы очень удивительно (например, удивительно для человека, пишущего код), если бы компилятор автоматически попытался перейти из a.p , что, вероятно, разрушило бы инварианты класса a . Это сработало бы, если бы вы return std::move(a.p); , но это явно крадет a.p .

Комментарии:

1. Исключение копирования может применяться к типам только для перемещения (как во втором примере OP)

2. @Barry ХОРОШО, мое первое предложение неверно. Исключение копирования также применяется к перемещениям. Тем не менее, я считаю, что остальная часть ответа остается в силе.

3. @AndreKostur: поскольку разработчик явно пытается вернуть unique_ptr, конструктор копирования которого удален, единственным вариантом является перемещение, и удаление копирования должно работать нормально (IMO). Что меня действительно удивило, так это ошибка компиляции, а не предупреждение.