Позвольте мне попытаться указать различные жизнеспособные способы передачи указателей на объекты, память которых управляется экземпляром шаблона класса std::unique_ptr
; он также применяется к старому шаблону класса std::auto_ptr
(который, я считаю, разрешает все виды использования, которые делает уникальный указатель, но для которого, кроме того, будут приниматься модифицируемые значения lvalue, где ожидаются значения rvalue, без необходимости вызова std::move
), в некоторой степени также std::shared_ptr
.
В качестве конкретного примера для обсуждения я рассмотрю следующий простой тип списка
struct node;
typedef std::unique_ptr<node> list;
struct node { int entry; list next; }
Экземпляры такого списка (которым нельзя разрешить делиться деталями с другими экземплярами или быть круглыми) полностью принадлежат тому, кто имеет начальный указатель list
. Если клиентский код знает, что список, который он хранит, никогда не будет пустым, он также может выбрать сохранение первого node
напрямую, а не list
.
Не нужно определять деструктор для 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)
.
Если указатель гарантированно не равен нулю (а это не так, поскольку списки могут быть пустыми), можно предпочесть передать ссылку, а не указатель. Это может быть указатель / ссылка на non- const
, если функции необходимо обновить содержимое узла (ов), без добавления или удаления какого-либо из них (последний будет включать владение).
Интересным случаем, который попадает в категорию режима 0, является создание (глубокой) копии списка; хотя функция, выполняющая это, должна, конечно, передавать право собственности на копию, которую она создает, она не связана с владением списком, который она копирует. Так что это можно определить следующим образом:
list copy(const node* p)
{ return list( p==nullptr ? nullptr : new node{p->entry,copy(p->next.get())} ); }
Этот код заслуживает более пристального взгляда, и на вопрос, почему он вообще компилируется (результат рекурсивного вызова copy
в списке инициализаторов связывается с ссылочным аргументом rvalue в конструкторе перемещения unique_ptr<node>
, он же list
, при инициализации поля next
сгенерированного node
) и на вопрос о том, почему он безопасен для исключений (если во время процесса рекурсивного выделения памяти заканчивается и какой-то вызов new
Выдает std::bad_alloc
, затем указатель на частично составленный список анонимно сохраняется во временном хранилище типа list
, созданном для списка инициализатора, и его деструктор очистит этот частичный список). Между прочим, следует сопротивляться искушению заменить (как я изначально это сделал) второй nullptr
на p
, который в конце концов, как известно, является нулевым: нельзя создать умный указатель из (необработанного) указателя до постоянной , даже если известно, что оно равно нулю.
Режим 1: передать умный указатель по значению
Функция, которая принимает значение умного указателя в качестве аргумента, получает объект, на который указывает сразу: умный указатель, который удерживал вызывающий объект (в именованной переменной или во временном анонимном), копируется в значение аргумента при входе в функцию и указатель вызывающего абонента стал нулевым (в случае временного копирования копия могла быть удалена, но в любом случае вызывающий абонент потерял доступ к указанному объекту). Я хотел бы назвать этот режим вызов наличными : абонент оплачивает аванс за вызываемую услугу и не может иметь никаких иллюзий относительно владения после вызова. Чтобы сделать это понятным, правила языка требуют, чтобы вызывающая сторона заключила аргумент в std::move
, если умный указатель содержится в переменной (технически, если аргумент является lvalue); в этом случае (но не для режима 3 ниже) эта функция делает то, что предлагает ее имя, а именно, перемещает значение из переменной во временное, оставляя переменную нулевой.
Для случаев, когда вызываемая функция безоговорочно принимает владение (воровство) указанным объектом, этот режим, используемый с std::unique_ptr
или std::auto_ptr
, является хорошим способом передачи указателя вместе с его владельцем, что позволяет избежать любого риска утечек памяти. Тем не менее, я думаю, что только в очень немногих ситуациях режим 3 не является предпочтительным (хотя бы немного) по сравнению с режимом 1. По этой причине я не буду приводить примеры использования этого режима. (Но см. Пример reversed
режима 3 ниже, где отмечается, что режим 1 будет работать как минимум так же хорошо.) Если функция принимает больше аргументов, чем только этот указатель, может случиться так, что кроме этого будет техническая причина избегать режима 1 (с std::unique_ptr
или std::auto_ptr
): поскольку фактическая операция перемещения происходит при передаче переменной-указателя p
по выражению std::move(p)
, нельзя допустить, чтобы p
имеет полезное значение при оценке других аргументов (порядок оценки не указан), что может привести к незначительным ошибкам; В отличие от этого, использование режима 3 гарантирует, что перед вызовом функции не происходит никакого перехода от p
, поэтому другие аргументы могут безопасно получить доступ к значению через p
.
При использовании с std::shared_ptr
этот режим интересен тем, что с одним определением функции он позволяет вызывающему выбирать , сохранять ли разделяемую копию указателя для себя при создании новой разделяемой копии для использования функцией (это происходит, когда предоставляется аргумент lvalue; конструктор копирования для общих указателей, используемых при вызове, увеличивает счетчик ссылок), или просто дает функции копию указателя, не сохраняя ее или не касаясь ссылки count (это происходит, когда предоставляется аргумент 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
типом интеллектуального указателя невозможно; тем не менее это преобразование может быть получено с помощью простой функции шаблона, если это действительно необходимо; см. https://stackoverflow.com/a/24868376/1436796). Для случая, когда вызываемая функция намеревается безоговорочно завладеть объектом, крадя у аргумента, обязательство предоставить аргумент lvalue дает неправильный сигнал: переменная не будет иметь полезного значения после Таким образом, режим 3, который предоставляет идентичные возможности внутри нашей функции, но просит вызывающих абонентов предоставить rvalue, должен быть предпочтительным для такого использования.
Однако существует действительный вариант использования для режима 2, а именно функции, которые могут изменять указатель или объект, указывающий на таким образом, что подразумевает владение . Например, функция, которая префиксирует узел к 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
, он все равно будет содержать исходный список.
Это был конструктивный пример; подмигнув на этот вопрос , можно также привести более разрушительный пример удаления первого узла, содержащего данное значение, если оно есть:
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
, содержащийся в удаляемом узле, не связан (release
, который возвращает указатель, но делает исходный ноль) до того, как reset
(неявно) уничтожит этот узел (когда он уничтожает старое значение, хранящееся в p
), гарантируя, что один и только один узел будут уничтожены в это время. (В альтернативной форме, упомянутой в комментарии, это время будет оставлено на усмотрение реализации оператора присваивания перемещения 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
был сброшен в ноль.В связи с переходом с режима 2 на 3 при передаче аргументов этот старый код должен быть переписан в std::shared_ptr<T> q(std::move(p))
и затем продолжит работу.Я понимаю, что комитету не понравился режим 2 здесь, но у него была возможность перейти в режим 1, определив вместо этого std::shared_ptr<T>(auto_ptr<T> p)
, они могли бы гарантировать, что старый код работает без изменений, потому что (в отличие от уникальных указателей) auto-поинтеры могут быть автоматически разыменованы со значением (сам объект указателя в процессе сбрасывается до нуля).Очевидно, комитет так сильно предпочел пропагандировать режим 3, а не режим 1, поэтому он решил активно нарушать существующий код , а не использовать режим 1 даже для уже устаревшего использования.
Когда предпочитать режим3 over mode 1
Режим 1 идеально подходит для использования во многих случаях и может быть предпочтительнее режима 3 в тех случаях, когда принятие владения в противном случае принимает форму перемещения интеллектуального указателя на локальную переменную, как в * 1205.* пример выше.Однако я вижу две причины предпочесть режим 3 в более общем случае:
Несколько эффективнее передать ссылку, чем создать временный и удалить старый указатель (обработка старого)наличные деньги несколько трудоемки);в некоторых сценариях указатель может быть передан несколько раз без изменений в другую функцию, прежде чем он будет фактически похищен.Такое прохождение обычно требует записи std::move
(если не используется режим 2), но обратите внимание, что это просто приведение, которое фактически ничего не делает (в частности, не разыменовывается), поэтому к нему добавлена нулевая стоимость.
Если возможно, что что-либо создает исключение между началом вызова функции и точкой, в которой оно (или некоторый содержащийся в нем вызов) фактически перемещает указанный объект в другую структуру данных (и это исключениееще не перехвачен внутри самой функции), то при использовании режима 1 объект, на который указывает умный указатель, будет уничтожен до того, как предложение catch
сможет обработать исключение (поскольку параметр функции был уничтожен при разматывании стека), но непоэтому при использовании режима 3. Последний дает вызывающей стороне возможность восстановить данные объекта в таких случаях (путем перехвата исключения).Обратите внимание, что режим 1 здесь не вызывает утечку памяти , но может привести к безвозвратной потере данных для программы, что также может быть нежелательным.
Возврат умного указателя: всегда по значению
Чтобы заключить слово о , возвращающем умный указатель, предположительно указывающий на объект, созданный для использования вызывающей стороной.Это на самом деле не сравнимо с передачей указателей на функции, но для полноты я хотел бы подчеркнуть, что в таких случаях всегда возвращает значение (и не используют std::move
в заявлении return
).Никто не хочет получить ссылку на указатель, который, вероятно, только что был отменен.