2010-11-30 3 views
232

unique_ptr<T> не позволяет создавать копии, вместо этого поддерживает семантику перемещения. Тем не менее, я могу вернуть unique_ptr<T> из функции и присвоить возвращаемое значение переменной.Возвращение unique_ptr из функций

#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, поскольку возвращаемое значение является временным объектом, который будет уничтожен, как только функция выйдет, тем самым гарантируя уникальность возвращаемого указателя. Мне интересно, как это реализовано, является ли оно особенным в компиляторе или есть какое-то другое предложение в спецификации языка, которое это использует?

+0

Гипотетически, если вы применяли метод * factory *, предпочитаете ли вы 1 или 2 возвращать заводские данные? Я полагаю, что это было бы наиболее распространенным использованием 1, потому что, с надлежащей фабрикой, вы действительно хотите, чтобы собственность на конструированную вещь передавалась вызывающему. – Xharlie 2015-09-15 11:10:12

+5

@ Xharlie? Они оба передают право собственности на `unique_ptr`.Весь вопрос в том, что 1 и 2 являются двумя разными способами достижения одного и того же. – Praetorian 2015-09-15 16:28:07

ответ

155

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

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

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

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


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

74

Это не имеет никакого отношения к unique_ptr, но относится к любому подвижному классу. Это гарантируется языковыми правилами, так как вы возвращаетесь по значению. Компилятор пытается исключить копии, вызывает конструктор перемещения, если он не может удалить копии, вызывает конструктор копирования, если он не может двигаться, и не может скомпилироваться, если он не может копировать.

Если у вас есть функция, которая принимает 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 
} 
+2

@Fred - ну, не совсем. Хотя `p` не является временным, результатом` foo() `, то, что возвращается, является; таким образом, это значение rvalue и может быть перемещено, что делает назначение в «main» возможным. Я бы сказал, что вы ошибались, за исключением того, что Nikola, похоже, применяет это правило к `p` самому, что IS по ошибке. – 2010-11-30 18:39:31

+0

Точно то, что я хотел сказать, но не мог найти слова. Я удалил эту часть ответа, поскольку это было не очень ясно. – 2010-11-30 18:43:01

+0

У меня вопрос: в исходном вопросе есть ли существенная разница между Line `1` и Line` 2`? На мой взгляд, это так же, поскольку при построении `p` в` main`, он заботится только о типе возвращаемого типа `foo`, правильно? – 2014-09-24 02:02:51

32

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

unique_ptr::unique_ptr(unique_ptr && src); 

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

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

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

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

1

Одна вещь, которую я не видел в других ответов Для уточнения another answers, что есть разница между возвращающей станд :: 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)); 
1

Я думаю, что это прекрасно описано в пункте 25 из Скотт Мейерс Effective Modern C++. Вот выдержка:

Часть Стандарта благословение РВО продолжает говорить, что если условия для РВО будут выполнены, но составители решили не выполнять копирование Пропуска, объект возвращаемым должен рассматриваться как Rvalue. По сути, Стандарт требует, чтобы при разрешении RVO выполнялось либо копирование, либо std::move неявно применяется к возвращаемым локальным объектам.

Здесь РВО относится к возвратной оптимизации стоимости и , если условия для РВО выполнены средства, возвращающегося локальный объект, объявленного внутри функции, что можно было бы ожидать, чтобы сделать РВЫ, что также хорошо объяснено в пункте 25 его книги, ссылаясь на стандарт (здесь локальный объект включает в себя временные объекты, созданные оператором return). Самый большой отрывок из выдержки - выполняется либо копирование, либо std::move неявно применяется к возвращаемым локальным объектам. Скотт упоминает в пункте 25, что std::move неявно применяется, когда компилятор предпочитает не удалять копию, и программист не должен явно делать это.

В вашем случае код явно кандидатом на РВО как он возвращает локальный объект p и тип p такой же, как тип возвращаемого значения, что приводит к копии элизии. И если компилятор решил не удалять копию, по какой-то причине std::move вышвырнул бы на строку 1.