Позвольте мне попытаться указать различные жизнеспособные способы передачи указателей на объекты, память которых управляется экземпляром std::unique_ptr
шаблона класса; это также относится к старому std::auto_ptr
шаблону класса (который, я считаю, разрешает все виды использования, которые делает уникальный указатель, но для которого, кроме того, будут приниматься модифицируемые значения lvalue, где ожидаются значения rval без необходимости вызова std::move
), и в некоторой степени также std::shared_ptr
.
В качестве конкретного примера для обсуждения я рассмотрю следующий простой тип списка
struct node;
typedef std::unique_ptr<node> list;
struct node { int entry; list next; }
Экземпляры такого списка (которым нельзя разрешить делиться частями с другими экземплярами или быть круглыми) полностью принадлежат тому, кто имеет начальный list
указатель. Если клиентский код знает, что список, который он хранит, никогда не будет пустым, он также может предпочесть сохранять первый node
напрямую, а не a list
. Деструктор для определения не node
требуется: поскольку деструкторы для его полей вызываются автоматически, умный указатель-деструктор рекурсивно удаляет весь список по окончании времени жизни начального указателя или узла.
Этот рекурсивный тип дает возможность обсудить некоторые случаи, которые менее заметны в случае умного указателя на простые данные. Также сами функции иногда предоставляют (рекурсивно) пример клиентского кода. Typedef для, list
конечно, смещен в сторону unique_ptr
, но определение может быть изменено для использования auto_ptr
или shared_ptr
вместо этого без особой необходимости переходить к тому, что сказано ниже (особенно в отношении обеспечения безопасности исключений без необходимости писать деструкторы).
Режимы передачи умных указателей вокруг
Режим 0: передать указатель или ссылочный аргумент вместо умного указателя
Если ваша функция не связана с владением, это предпочтительный метод: вообще не заставляйте ее использовать умный указатель. В этом случае вашей функции не нужно беспокоиться о том, кто владеет указанным объектом или каким образом осуществляется управление владением, поэтому передача необработанного указателя является как совершенно безопасной, так и наиболее гибкой формой, поскольку независимо от владельца клиент всегда может создать необработанный указатель (либо путем вызова get
метода, либо из оператора address-of &
).
Например, функция для вычисления длины такого списка должна давать не 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
если функция должна обновлять содержимое узла (ов), без добавления или удаления любого из них (последний будет включать в себя владение).
Интересным случаем, который попадает в категорию режима 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 предоставляется аргумент; конструктор копирования для общих указателей, используемых при вызове, увеличивает счетчик ссылок) или просто дает функции копию указателя, не сохраняя ее или не затрагивая счетчик ссылок (это происходит, когда предоставляется аргумент 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, который предоставляет идентичные возможности внутри нашей функции, но просит вызывающих абонентов предоставить значение r, должен быть предпочтительным для такого использования.
Однако существует действительный вариант использования для режима 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: передать умный указатель (изменяемая) ссылка на значение
Это предпочтительный режим для использования при владении указателем. Я хотел бы вызвать этот метод вызовом с помощью чека : вызывающая сторона должна принять отказ от владения, как если бы она предоставляла наличные, подписав чек, но фактическое снятие денег откладывается до тех пор, пока вызываемая функция не утащит указатель (точно так же, как при использовании режима 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;
}
Эта функция может быть вызвана как in l = reversed(std::move(l));
для обращения списка к самому себе, но обратный список также может использоваться по-другому.
Здесь аргумент немедленно перемещается в локальную переменную для эффективности (можно было бы использовать параметр l
непосредственно вместо него p
, но тогда доступ к нему каждый раз потребовал бы дополнительного уровня косвенности); следовательно, разница с передачей аргументов в режиме 1 минимальна. Фактически, используя этот режим, аргумент мог бы служить непосредственно локальной переменной, что позволило бы избежать этого начального перемещения; это всего лишь пример общего принципа, согласно которому, если аргумент, передаваемый по ссылке, служит только для инициализации локальной переменной, можно с тем же успехом передать ее по значению и использовать параметр в качестве локальной переменной.
Использование режима 3, как представляется, поддерживается стандартом, о чем свидетельствует тот факт, что все предоставляемые библиотечные функции передают владение интеллектуальными указателями с использованием режима 3. Конкретным убедительным примером является конструктор std::shared_ptr<T>(auto_ptr<T>&& p)
. Этот конструктор использовал (in std::tr1
) для получения модифицируемой ссылки lvalue (точно так же, как auto_ptr<T>&
конструктор копирования), и поэтому мог вызываться с auto_ptr<T>
lvalue p
как in std::shared_ptr<T> q(p)
, после чего p
был сброшен в null. В связи с переходом с режима 2 на 3 при передаче аргументов этот старый код теперь должен быть переписан std::shared_ptr<T> q(std::move(p))
и затем будет продолжать работать. Я понимаю, что комитету здесь не понравился режим 2, но у него была возможность перейти в режим 1, определивstd::shared_ptr<T>(auto_ptr<T> p)
вместо этого они могли бы гарантировать, что старый код работает без изменений, потому что (в отличие от уникальных указателей) автоматические указатели могут быть автоматически разыменованы со значением (сам объект указателя в процессе сбрасывается до нуля). Очевидно, комитет так сильно предпочел пропагандировать режим 3, а не режим 1, что он решил активно нарушать существующий код, а не использовать режим 1 даже для уже устаревшего использования.
Когда предпочитать режим 3 над режимом 1
Режим 1 идеально подходит для использования во многих случаях и может быть предпочтительным по сравнению с режимом 3 в тех случаях, когда принятие владения в противном случае принимает форму перемещения интеллектуального указателя на локальную переменную, как в reversed
примере выше. Однако я вижу две причины предпочесть режим 3 в более общем случае:
Несколько эффективнее передать ссылку, чем создать временный указатель и убрать старый указатель (обработка денег несколько трудоемка); в некоторых сценариях указатель может быть передан несколько раз без изменений в другую функцию, прежде чем он будет фактически похищен. Такое прохождение обычно требует записи std::move
(если не используется режим 2), но обратите внимание, что это просто приведение, которое фактически ничего не делает (в частности, без разыменования), поэтому к нему прилагается нулевая стоимость.
Если это возможно, что что-либо создает исключение между началом вызова функции и точкой, в которой оно (или некоторый содержащийся в нем вызов) фактически перемещает объект, на который указывает указатель, в другую структуру данных (и это исключение еще не перехвачено внутри самой функции). ), то при использовании режима 1 объект, на который ссылается умный указатель, будет уничтожен до того, как catch
предложение сможет обработать исключение (поскольку параметр функции был уничтожен при разматывании стека), но не при использовании режима 3. Последний дает вызывающая сторона имеет возможность восстановить данные объекта в таких случаях (путем перехвата исключения). Обратите внимание, что режим 1 здесь не вызывает утечку памяти , но может привести к невосстановимой потере данных для программы, что также может быть нежелательным.
Возврат умного указателя: всегда по значению
Чтобы завершить слово о возвращении умного указателя, предположительно указывающего на объект, созданный для использования вызывающей стороной. Это на самом деле не сравнимо с передачей указателей на функции, но для полноты я хотел бы настаивать на том, чтобы в таких случаях всегда возвращалось значение (и не использовалось std::move
в return
выражении). Никто не хочет получить ссылку на указатель, который, вероятно, только что был отменен.