Я новичок в перемещении семантики в С++ 11, и я не очень хорошо знаю, как обрабатывать параметры unique_ptr
в конструкторах или функциях. Рассмотрим этот класс, ссылающийся на себя:
Ответ 1
Ниже перечислены возможные способы использования уникального указателя как аргумента, а также связанного с ним значения.
(A) По значению
Base(std::unique_ptr<Base> n)
: next(std::move(n)) {}
Чтобы пользователь мог вызвать это, они должны выполнить одно из следующих действий:
Base newBase(std::move(nextBase));
Base fromTemp(std::unique_ptr<Base>(new Base(...));
Чтобы получить уникальный указатель по значению, вы передаете право собственности на указатель на интересующую функцию/объект/etc. После того как newBase
построено, nextBase
гарантированно будет пустым. Вы не являетесь владельцем объекта, и у вас даже нет указателя на него. Он ушел.
Это обеспечивается, потому что мы берем параметр по значению. std::move
фактически ничего не движет; это просто причудливый актерский состав. std::move(nextBase)
возвращает a Base&&
, который является ссылкой r-value на nextBase
. Это все, что он делает.
Поскольку Base::Base(std::unique_ptr<Base> n)
принимает свой аргумент по значению, а не по ссылке r-value, С++ автоматически создаст временную для нас. Он создает std::unique_ptr<Base>
из Base&&
, который мы дали функции через std::move(nextBase)
. Конструкция этого временного объекта фактически переводит значение из nextBase
в аргумент функции n
.
(B) Неконстантной ссылкой l-value
Base(std::unique_ptr<Base> &n)
: next(std::move(n)) {}
Это должно быть вызвано фактическим значением l (именованной переменной). Его нельзя вызвать с временным типом:
Base newBase(std::unique_ptr<Base>(new Base)); //Illegal in this case.
Смысл этого такой же, как и значение любого другого использования неконстантных ссылок: функция может или не может претендовать на владение указателем. С учетом этого кода:
Base newBase(nextBase);
Нет гарантии, что nextBase
пуст. Он может быть пустым; это не так. Это действительно зависит от того, что хочет сделать Base::Base(std::unique_ptr<Base> &n)
. Из-за этого, не очень очевидно только из сигнатуры функции, что произойдет; вы должны прочитать реализацию (или соответствующую документацию).
Из-за этого я бы не предложил это как интерфейс.
(C) По ссылке const l-value
Base(std::unique_ptr<Base> const &n);
Я не показываю реализацию, потому что вы не можете перейти от const&
. Передавая const&
, вы говорите, что функция может обращаться к Base
с помощью указателя, но не может его хранить в любом месте. Он не может претендовать на право собственности на него.
Это может быть полезно. Не обязательно для вашего конкретного случая, но всегда хорошо иметь возможность передать кому-то указатель и знать, что они не могут (без нарушения правил С++, например, без отбрасывания const
) претендовать на право собственности на него. Они не могут его хранить. Они могут передавать их другим, но эти другие должны соблюдать те же правила.
(D) По ссылке r-value
Base(std::unique_ptr<Base> &&n)
: next(std::move(n)) {}
Это более или менее идентично случаю "не-const l-value reference". Различия - это две вещи.
-
Вы можете передать временное:
Base newBase(std::unique_ptr<Base>(new Base)); //legal now..
-
Вы должны использовать std::move
при передаче не временных аргументов.
Последнее действительно проблема. Если вы видите эту строку:
Base newBase(std::move(nextBase));
У вас есть разумное ожидание, что после завершения этой строки nextBase
должно быть пустым. Его следовало перенести. В конце концов, вы сидите там std::move
, сообщая вам, что движение произошло.
Проблема в том, что у нее нет. От него не гарантируется переход. Возможно, это было перемещено, но вы будете знать только, посмотрев исходный код. Вы не можете сказать только по сигнатуре функции.
Рекомендации
- (A) По значению: Если вы хотите, чтобы функция требовала права собственности на
unique_ptr
, возьмите ее по значению.
- (C) По ссылке const l-value: Если вы хотите, чтобы функция просто использовала
unique_ptr
для продолжительности выполнения этой функции, возьмите ее const&
. В качестве альтернативы, передайте &
или const&
фактическому типу, на который указывает, вместо использования unique_ptr
.
- (D) По ссылке r-value: Если функция может или не может претендовать на право собственности (в зависимости от внутренних путей кода), тогда возьмите ее
&&
. Но я настоятельно рекомендую это делать, когда это возможно.
Как манипулировать unique_ptr
Вы не можете скопировать unique_ptr
. Вы можете только переместить его. Правильный способ сделать это с помощью стандартной библиотеки std::move
.
Если вы берете unique_ptr
по значению, вы можете свободно перемещаться от него. Но движение на самом деле не происходит из-за std::move
. Возьмем следующее утверждение:
std::unique_ptr<Base> newPtr(std::move(oldPtr));
Это действительно два утверждения:
std::unique_ptr<Base> &&temporary = std::move(oldPtr);
std::unique_ptr<Base> newPtr(temporary);
(примечание: вышеупомянутый код технически не компилируется, так как невременные ссылки r-значения не являются фактическими значениями r. Это здесь только для демонстрационных целей).
temporary
- это всего лишь ссылка r-value на oldPtr
. Он находится в конструкторе newPtr
, где происходит движение. unique_ptr
move constructor (конструктор, который принимает &&
для себя), является тем, что делает фактическое движение.
Если у вас есть значение unique_ptr
и вы хотите его где-то сохранить, вы должны использовать std::move
для хранения.
Ответ 2
Позвольте мне попытаться указать различные жизнеспособные способы передачи указателей на объекты, память которых управляется экземпляром шаблона класса std::unique_ptr
; это также относится к более старому шаблону класса std::auto_ptr
(который, я считаю, допускает все применения, которые использует уникальный указатель, но для которых, кроме того, будут приняты модифицируемые значения ll, где ожидаются значения r, без необходимости ссылаться на std::move
), и в некоторой степени также до std::shared_ptr
.
В качестве конкретного примера для обсуждения я рассмотрю следующий простой тип списка
struct node;
typedef std::unique_ptr<node> list;
struct node { int entry; list next; }
Экземпляры такого списка (который нельзя разрешить обменивать частями с другими экземплярами или быть круговыми) полностью принадлежат тому, кто имеет начальный указатель list
. Если код клиента знает, что список, который он хранит, никогда не будет пустым, он также может выбрать первый node
, а не list
.
Деструктор для node
не должен определяться: поскольку деструкторы для его полей будут автоматически вызваны, весь список будет рекурсивно удален деструктором интеллектуального указателя после завершения жизненного цикла исходного указателя или node.
Этот рекурсивный тип дает возможность обсудить некоторые случаи, которые менее заметны в случае умного указателя на простые данные. Также сами функции иногда предоставляют (рекурсивно) пример кода клиента. Typedef для list
, конечно, смещен в сторону unique_ptr
, но определение может быть изменено для использования auto_ptr
или shared_ptr
вместо этого без особых изменений в соответствии с тем, что сказано ниже (особенно в отношении безопасности исключений без нужно написать деструкторы).
Режимы передачи интеллектуальных указателей вокруг
Режим 0: передать указатель или ссылочный аргумент вместо интеллектуального указателя
Если ваша функция не связана с правами собственности, это предпочтительный метод: не заставляйте его использовать смарт-указатель. В этом случае вашей функции не нужно беспокоиться о том, кому принадлежит объект, на который указывает, или каким образом это управление владением, поэтому передача необработанного указателя является абсолютно безопасной и наиболее гибкой формой, поскольку независимо от права собственности клиент всегда может производят необработанный указатель (либо путем вызова метода get
, либо из адреса-оператора &
).
Например, функция для вычисления длины такого списка не должна указывать аргумент list
, а необработанный указатель:
size_t length(const node* p)
{ size_t l=0; for ( ; p!=nullptr; p=p->next.get()) ++l; return l; }
Клиент, который содержит переменную list head
, может вызывать эту функцию как length(head.get())
,
в то время как клиент, который выбрал вместо него node n
, представляющий непустой список, может вызвать length(&n)
.
Если указатель гарантированно будет недействительным (это не так, потому что списки могут быть пустыми), возможно, предпочтительнее передать ссылку, а не указатель. Это может быть указатель/ссылка на не const
, если функции необходимо обновить содержимое node (s) без добавления или удаления каких-либо из них (последнее связано с владением).
Интересным случаем, который попадает в категорию mode 0, является (глубокая) копия списка; в то время как функция, выполняющая это, должна, конечно же, передать право собственности на созданную копию, она не связана с правом собственности на список, который он копирует. Поэтому его можно определить следующим образом:
list copy(const node* p)
{ return list( p==nullptr ? nullptr : new node{p->entry,copy(p->next.get())} ); }
Этот код заслуживает пристального взгляда, как на вопрос о том, почему он вообще компилируется (результат рекурсивного вызова copy
в списке инициализации привязывается к аргументу ссылки rvalue в конструкторе перемещения unique_ptr<node>
, aka list
, при инициализации поля next
сгенерированного node
) и для вопроса о том, почему он безопасен для исключения (если во время рекурсивного процесса выделения памяти заканчивается и некоторый вызов new
throws std::bad_alloc
, тогда в это время указатель на частично построенный список удерживается анонимно во временном типе list
, созданном для списка инициализации, и его деструктор очистит этот неполный список). Кстати, нужно устоять перед соблазном заменить (как я изначально сделал) второй nullptr
на p
, который, как известно, в этом случае является нулевым: нельзя построить интеллектуальный указатель из (необработанного) указателя к константе, даже если она известна как null.
Режим 1: передать интеллектуальный указатель по значению
Функция, которая принимает значение интеллектуального указателя в качестве аргумента, получает доступ к объекту, на который указывает сразу: умный указатель, который удерживает вызывающий объект (в именованной переменной или анонимный временной), копируется в значение аргумента при входе функции и указатель вызывающего абонента стал нулевым (в случае временной копии, возможно, была удалена копия, но в любом случае вызывающий абонент потерял доступ к указанному объекту). Я бы назвал этот режим вызовом денежными средствами: вызывающий абонент платит перед вызовом службы и не может иметь иллюзий относительно права собственности после вызова. Чтобы это было ясно, правила языка требуют, чтобы вызывающий оператор обернул аргумент в std::move
, если интеллектуальный указатель удерживается в переменной (технически, если аргумент является lvalue); в этом случае (но не для режима 3 ниже) эта функция делает то, что предлагает его имя, а именно, перемещает значение из переменной во временное, оставляя переменную null.
В случаях, когда вызываемая функция безоговорочно принимает на себя (накладывает) заостренный объект, этот режим, используемый с std::unique_ptr
или std::auto_ptr
, является хорошим способом передачи указателя вместе с его собственностью, что позволяет избежать любого риска утечек памяти. Тем не менее, я думаю, что существует очень мало ситуаций, когда режим 3 ниже не должен быть предпочтительным (когда-либо слегка) в режиме 1. По этой причине я не буду приводить примеры использования этого режима. (Но см. Пример reversed
примера режима 3 ниже, где отмечено, что режим 1 будет делать, по крайней мере, также.) Если функция принимает больше аргументов, чем только этот указатель, может случиться, что есть еще a техническая причина, чтобы избежать режима 1 (с помощью std::unique_ptr
или std::auto_ptr
): поскольку фактическая операция перемещения выполняется при передаче указательной переменной p
выражением std::move(p)
, нельзя предположить, что p
содержит полезное значение при оценке других аргументов (порядок оценки неуточнен), что может привести к незначительным ошибкам; напротив, использование режима 3 гарантирует, что перед вызовом функции не происходит перехода из p
, поэтому другие аргументы могут безопасно получить доступ к значению через p
.
При использовании с std::shared_ptr
этот режим интересен тем, что с определением одной функции он позволяет вызывающему пользователю выбирать, сохранять ли копию общего указателя для себя, создавая новую копию общего доступа, которая будет использоваться функцией (это происходит, когда предоставляется аргумент lvalue, конструктор копирования для общих указателей, используемых при вызове, увеличивает счетчик ссылок) или просто дает функции копию указателя без сохранения одного или касания счетчика ссылок (это происходит, когда предоставляется аргумент rvalue, возможно, lvalue, заключенный в вызов std::move
). Например,
void f(std::shared_ptr<X> x) // call by shared cash
{ container.insert(std::move(x)); } // store shared pointer in container
void client()
{ std::shared_ptr<X> p = std::make_shared<X>(args);
f(p); // lvalue argument; store pointer in container but keep a copy
f(std::make_shared<X>(args)); // prvalue argument; fresh pointer is just stored away
f(std::move(p)); // xvalue argument; p is transferred to container and left null
}
То же самое может быть достигнуто путем отдельного определения void f(const std::shared_ptr<X>& x)
(для случая lvalue) и void f(std::shared_ptr<X>&& x)
(для случая rvalue), с телами функций, отличающимися только тем, что первая версия вызывает семантику копирования (используя построение/присвоение копирования при использовании x
), но вторая версия переносит семантику (вместо записи вместо std::move(x)
, как в примере кода). Поэтому для общих указателей режим 1 может быть полезен, чтобы избежать дублирования кода.
Режим 2: передать интеллектуальный указатель по (изменяемой) ссылке lvalue
Здесь функция просто требует наличия модифицируемой ссылки на интеллектуальный указатель, но не дает указания на то, что она с ней сделает. Я бы назвал этот метод вызовом по карте: вызывающий обеспечивает оплату, указав номер кредитной карты. Ссылка может использоваться, чтобы взять на себя ответственность за объект с указателем, но этого не нужно. Этот режим требует предоставления модифицируемого аргумента lvalue, соответствующего тому факту, что желаемый эффект функции может включать в себя оставление полезного значения в переменной аргумента. Вызывающий вызов с выражением rvalue, который он хочет передать такой функции, будет вынужден сохранить его в именованной переменной, чтобы иметь возможность сделать вызов, поскольку язык предоставляет только неявное преобразование в константную ссылку lvalue (ссылаясь на временную ) из rvalue. (В отличие от противоположной ситуации, обработанной std::move
, приведение из Y&&
в Y&
, с Y
типа интеллектуального указателя, невозможно, тем не менее это преобразование может быть получено простой функцией шаблона, если это действительно необходимо; см. fooobar.com/questions/4280/...). В случае, когда вызываемая функция намерена безоговорочно взять на себя ответственность за объект, воровав из аргумента, обязательство предоставить аргумент lvalue указывает неверный сигнал: после вызова не будет полезной переменной. Поэтому режим 3, который дает идентичные возможности внутри нашей функции, но запрашивает у абонентов предоставление rvalue, должен быть предпочтительным для такого использования.
Однако для режима 2 существует допустимый прецедент, а именно функции, которые могут изменять указатель, или объект, на который указывает объект, который связан с владением. Например, функция, которая префикс node на list
предоставляет пример такого использования:
void prepend (int x, list& l) { l = list( new node{ x, std::move(l)} ); }
Очевидно, что здесь было бы нежелательно заставить вызывающих абонентов использовать std::move
, поскольку их интеллектуальный указатель по-прежнему владеет четко определенным и непустым списком после вызова, хотя и отличается от предыдущего.
Снова интересно наблюдать, что произойдет, если вызов prepend
завершится неудачей из-за отсутствия свободной памяти. Затем вызов new
будет бросать std::bad_alloc
; в этот момент времени, так как не может быть выделено node
, несомненно, что переданное значение rvalue (режим 3) из std::move(l)
еще не может быть похищено, поскольку это будет сделано для построения поля next
node
, которые не были выделены. Таким образом, первоначальный интеллектуальный указатель l
по-прежнему содержит исходный список при ошибке; этот список будет либо правильно уничтожен деструктором интеллектуального указателя, либо в случае, если l
должен выжить благодаря достаточно раннему предложению catch
, он все равно сохранит исходный список.
Это был конструктивный пример; подмигивая этому вопросу, можно также дать более разрушительный пример удаления первого node, содержащего заданное значение, если оно есть:
void remove_first(int x, list& l)
{ list* p = &l;
while ((*p).get()!=nullptr and (*p)->entry!=x)
p = &(*p)->next;
if ((*p).get()!=nullptr)
(*p).reset((*p)->next.release()); // or equivalent: *p = std::move((*p)->next);
}
Снова правильность здесь довольно тонкая. Примечательно, что в заключительном заявлении указатель (*p)->next
, содержащийся внутри node, который должен быть удален, отключен (через release
, который возвращает указатель, но делает исходный null) до reset
(неявно) уничтожает, что node (когда он уничтожает старое значение, удерживаемое p
), гарантируя, что один и только один node будет уничтожен в это время. (В альтернативной форме, упомянутой в комментарии, это время будет оставлено для внутренних операций реализации оператора присваивания перемещения экземпляра std::unique_ptr
list
, стандарт говорит 20.7.1.2.3; 2, что этот оператор должен действовать "как бы путем вызова reset(u.release())
", поэтому время также должно быть безопасным.)
Обратите внимание, что prepend
и remove_first
не могут быть вызваны клиентами, которые хранят локальную переменную node
для всегда непустого списка, и правильно, так как данные реализации не могут работать для таких случаев.
Режим 3: передать интеллектуальный указатель с помощью (изменяемой) справки rvalue
Это предпочтительный режим использования, когда вы просто берете на себя ответственность за указатель. Я бы назвал этот метод вызовом с помощью проверки: вызывающий должен принять отказ от владения, как если бы он предоставлял наличные деньги, подписывая чек, но фактический вывод откладывается до тех пор, пока вызываемая функция фактически не нападает на указатель (точно как это было бы при использовании режима 2). "Подписание чека" конкретно означает, что вызывающие абоненты должны обернуть аргумент в std::move
(как в режиме 1), если это значение lvalue (если это rvalue, часть "отказ от собственности" очевидна и не требует отдельного код).
Обратите внимание, что технически режим 3 ведет себя точно так же, как режим 2, поэтому вызываемая функция не должна принимать участие; однако я бы настаивал на том, что если есть какая-либо неопределенность в отношении передачи прав собственности (при нормальном использовании), режим 2 должен быть предпочтительнее режима 3, так что использование режима 3 является неявным сигналом для вызывающих лиц, что они отказываются от собственности. Можно было бы возразить, что передача только одного аргумента 1 действительно сигнализирует о принудительном потере права собственности вызывающим абонентам. Но если у клиента есть какие-то сомнения в намерениях вызываемой функции, она должна знать спецификации вызываемой функции, что должно устранить любые сомнения.
На удивление сложно найти типичный пример, включающий наш тип list
, который использует передачу аргументов режима 3. Перемещение списка b
в конец другого списка a
является типичным примером; однако a
(который выживает и удерживает результат операции) лучше передается с использованием режима 2:
void append (list& a, list&& b)
{ list* p=&a;
while ((*p).get()!=nullptr) // find end of list a
p=&(*p)->next;
*p = std::move(b); // attach b; the variable b relinquishes ownership here
}
Чистым примером передачи аргументов режима 3 является следующее, которое берет список (и его право собственности) и возвращает список, содержащий идентичные узлы в обратном порядке.
list reversed (list&& l) noexcept // pilfering reversal of list
{ list p(l.release()); // move list into temporary for traversal
list result(nullptr);
while (p.get()!=nullptr)
{ // permute: result --> p->next --> p --> (cycle to result)
result.swap(p->next);
result.swap(p);
}
return result;
}
Эта функция может быть вызвана как в l = reversed(std::move(l));
, чтобы перевернуть список в себя, но перевернутый список также можно использовать по-разному.
Здесь аргумент немедленно переносится в локальную переменную для эффективности (можно было использовать параметр l
непосредственно вместо p
, но при этом каждый раз доступ к нему включал бы дополнительный уровень косвенности); следовательно, разница с передачей аргумента режима 1 минимальна. Фактически, используя этот режим, аргумент мог бы служить как локальная переменная, тем самым избегая этого начального перемещения; это всего лишь пример общего принципа, что если аргумент, переданный по ссылке, служит только для инициализации локальной переменной, можно просто передать ее по значению вместо этого и использовать параметр как локальную переменную.
Использование режима 3, по-видимому, поддерживается стандартом, о чем свидетельствует тот факт, что все предоставленные библиотечные функции передают владение интеллектуальными указателями, использующими режим 3. Особым убедительным примером является конструктор std::shared_ptr<T>(auto_ptr<T>&& p)
. Этот конструктор использовал (в std::tr1
), чтобы взять модифицируемую ссылку lvalue (как и конструктор копирования auto_ptr<T>&
), и поэтому ее можно вызвать с помощью auto_ptr<T>
lvalue p
, как в std::shared_ptr<T> q(p)
, после чего p
был reset равным null. Из-за перехода из режима 2 в 3 при передаче аргумента этот старый код теперь должен быть переписан на std::shared_ptr<T> q(std::move(p))
, а затем продолжит работу. Я понимаю, что комитету не понравился режим 2 здесь, но у них была возможность перейти в режим 1, вместо этого вместо std::shared_ptr<T>(auto_ptr<T> p)
они могли обеспечить, чтобы старый код работал без изменений, потому что (в отличие от уникальных указателей) auto - указатели могут быть разыменованы без разницы до значения (сам объект-указатель reset равен нулю в процессе). По-видимому, комитет так предпочитал защищать режим 3 в режиме 1, что они решили активно нарушать существующий код, а не использовать режим 1 даже для уже устаревшего использования.
Если выбрать режим 3 в режиме 1
Режим 1 отлично используется во многих случаях и может быть предпочтительнее режима 3 в тех случаях, когда предположение о том, что собственность в противном случае принимает форму перемещения умного указателя на локальную переменную, как в приведенном выше примере reversed
. Однако я могу видеть две причины предпочитать режим 3 в более общем случае:
-
Немного более эффективно передавать ссылку, чем создавать временный и nix старый указатель (обработка наличных денег несколько трудоемкая); в некоторых сценариях указатель может быть передан несколько раз без изменений в другую функцию до того, как он будет фактически похищен. Такая передача обычно требует записи std::move
(если не используется режим 2), но обратите внимание, что это просто актер, который на самом деле ничего не делает (в частности, не разыменование), поэтому он имеет нулевую стоимость.
-
Можно ли предположить, что что-то генерирует исключение между началом вызова функции и точкой, в которой он (или какой-то содержащийся вызов) фактически перемещает объект с указателем на другую структуру данных (и это исключение не является уже пойман внутри самой функции), то при использовании режима 1 объект, на который ссылается интеллектуальный указатель, будет уничтожен до того, как предложение catch
может обрабатывать исключение (поскольку параметр функции был разрушен во время разматывания стека), но не так при использовании режима 3. Последний дает вызывающему абоненту возможность восстановить данные объекта в таких случаях (путем обнаружения исключения). Обратите внимание, что режим 1 здесь не вызывает утечку памяти, но может привести к неустранимой потере данных для программы, что также может быть нежелательным.
Возврат умного указателя: всегда по значению
Завершите слово о возврате умного указателя, предположительно указывающего на объект, созданный для использования вызывающим. На самом деле это не случай, сравнимый с прохождением указателей на функции, но для полноты я хотел бы настаивать на том, что в таких случаях всегда возвращает значение (и не использует std::move
в return
выражение). Никто не хочет получать ссылку на указатель, который, вероятно, только что был добавлен.