Как передать аргумент unique_ptr конструктору или функции?


400

Я новичок в перемещении семантики в C ++ 11, и я не очень хорошо знаю, как обрабатывать unique_ptrпараметры в конструкторах или функциях. Рассмотрим этот класс, ссылающийся на себя:

#include <memory>

class Base
{
  public:

    typedef unique_ptr<Base> UPtr;

    Base(){}
    Base(Base::UPtr n):next(std::move(n)){}

    virtual ~Base(){}

    void setNext(Base::UPtr n)
    {
      next = std::move(n);
    }

  protected :

    Base::UPtr next;

};

Это как я должен писать функции, принимающие unique_ptrаргументы?

И мне нужно использовать std::moveв коде вызова?

Base::UPtr b1;
Base::UPtr b2(new Base());

b1->setNext(b2); //should I write b1->setNext(std::move(b2)); instead?


1
Разве это не ошибка сегментации, когда вы вызываете b1-> setNext для пустого указателя?
Балки

Ответы:


836

Вот возможные способы использования уникального указателя в качестве аргумента, а также их связанное значение.

(A) По значению

Base(std::unique_ptr<Base> n)
  : next(std::move(n)) {}

Чтобы пользователь мог вызвать это, он должен выполнить одно из следующих действий:

Base newBase(std::move(nextBase));
Base fromTemp(std::unique_ptr<Base>(new Base(...));

Получение уникального указателя по значению означает, что вы передаете право владения указателем на соответствующую функцию / объект / и т. Д. После того, newBaseкак построен, nextBaseгарантированно будет пустым . Вы не являетесь владельцем объекта, и у вас даже больше нет указателя на него. Это прошло.

Это гарантировано, потому что мы берем параметр по значению. std::moveна самом деле ничего не двигает ; это просто модный актерский состав. std::move(nextBase)возвращает , Base&&что является ссылкой на г-значение nextBase. Это все, что он делает.

Поскольку Base::Base(std::unique_ptr<Base> n)аргумент принимает значение по значению, а не по ссылке r-value, C ++ автоматически создаст для нас временный. Это создает std::unique_ptr<Base>из того, Base&&что мы дали функцию через std::move(nextBase). Именно конструкция этого временного объекта фактически перемещает значение nextBaseв аргумент функции n.

(B) по неконстантной l-значной ссылке

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) константным l-значением

Base(std::unique_ptr<Base> const &n);

Я не показываю реализацию, потому что вы не можете перейти от const&. Передавая const&, вы говорите, что функция может получить доступ Baseчерез указатель, но она не может хранить его где-либо. Он не может претендовать на владение им.

Это может быть полезно. Не обязательно для вашего конкретного случая, но всегда хорошо иметь возможность вручать кому-то указатель и знать, что он не может (не нарушая правил C ++, например, не отбрасывая const) претендовать на владение им. Они не могут хранить это. Они могут передать это другим, но эти другие должны соблюдать те же правила.

(D) по значению r

Base(std::unique_ptr<Base> &&n)
  : next(std::move(n)) {}

Это более или менее идентично случаю «по неконстантной ссылке на l-значение». Различия - это две вещи.

  1. Вы можете сдать временный:

    Base newBase(std::unique_ptr<Base>(new Base)); //legal now..
  2. Вы должны использовать std::moveпри передаче не временных аргументов.

Последнее действительно проблема. Если вы видите эту строку:

Base newBase(std::move(nextBase));

У вас есть разумное ожидание, что после завершения этой строки она nextBaseдолжна быть пустой. Это должно было быть перенесено из. В конце концов, вы std::moveсидите там и говорите, что движение произошло.

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

рекомендации

  • (A) По значению: если вы имеете в виду, что функция претендует на владение a unique_ptr, возьмите ее по значению.
  • (C) С помощью константной ссылки на l-значение: если вы хотите, чтобы функция просто использовала значение unique_ptrна время выполнения этой функции, сделайте это const&. В качестве альтернативы, передайте a &или 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-значение oldPtr. Именно в конструкторе из newPtrкоторых движение происходит. unique_ptrКонструктор перемещения (конструктор, который принимает &&сам к себе) - это то, что делает фактическое движение.

Если у вас есть unique_ptrзначение и вы хотите сохранить его где-то, вы должны использовать его std::moveдля хранения.


5
@Nicol: но std::moveне называет его возвращаемое значение. Помните, что именованные ссылки на rvalue являются lvalues. ideone.com/VlEM3
Р. Мартиньо Фернандес

31
Я в принципе согласен с этим ответом, но у меня есть некоторые замечания. (1) Я не думаю, что есть допустимый вариант использования для передачи ссылки на const lvalue: все, что может сделать вызываемый объект, он может сделать и со ссылкой на const (голый) указатель, или даже лучше сам указатель [и дело не в том, чтобы владеть собственностью unique_ptr; может быть, некоторые другие вызывающие абоненты нуждаются в той же функциональности, но удерживают shared_ptrвызов вместо] (2) по ссылке lvalue. Это может быть полезно, если вызываемая функция изменяет указатель, например, добавляя или удаляя (принадлежащие списку) узлы из связанного списка.
Марк ван Леувен

8
... (3) Хотя ваш аргумент в пользу передачи по значению по сравнению с передачей по ссылке rvalue имеет смысл, я думаю, что сам стандарт всегда передает unique_ptrзначения по ссылке rvalue (например, при преобразовании их в shared_ptr). Обоснованием этого может быть то, что он немного более эффективен (переход на временные указатели не выполняется), в то время как он дает точно такие же права вызывающей стороне (может передавать значения r или значения, заключенные в оболочку std::move, но не обнаженные значения lvalue).
Марк ван Леувен

19
Просто чтобы повторить то, что сказал Марк, и процитировал Саттера : «Не используйте const unique_ptr & в качестве параметра; используйте вместо него widget *»
Джон

17
Мы обнаружили проблему с побочным значением - перемещение происходит во время инициализации аргумента, которая неупорядочена относительно других вычислений аргумента (конечно, кроме в initializer_list). Принимая во внимание, что принятие ссылки на rvalue строго предписывает, чтобы перемещение происходило после вызова функции и, следовательно, после оценки других аргументов. Поэтому принятие ссылки rvalue должно быть предпочтительным всякий раз, когда будет принято право собственности.
Бен Фойгт

57

Позвольте мне попытаться указать различные жизнеспособные способы передачи указателей на объекты, память которых управляется экземпляром 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 будет работать как минимум так же хорошо.) Если функция принимает больше аргументов, чем только этот указатель, может случиться так, что кроме этого есть техническая причина, чтобы избежать режима 1std::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выражении). Никто не хочет получить ссылку на указатель, который, вероятно, только что был отменен.


1
+1 для режима 0 - передача базового указателя вместо unique_ptr. Немного не по теме (поскольку речь идет о передаче unique_ptr), но это просто и позволяет избежать проблем.
Мачта

« Режим 1 здесь не вызывает утечку памяти » - это означает, что режим 3 действительно вызывает утечку памяти, что не соответствует действительности. Независимо от того unique_ptr, был ли перемещен из или нет, он все равно будет приятно удалять значение, если он все еще будет удерживать его всякий раз, когда уничтожается или используется повторно.
rustyx

@RustyX: Я не понимаю, как вы истолковываете это значение, и я никогда не собирался говорить то, что вы думаете, я имел в виду. Все, что я имел в виду, это то, что, как и везде, использование unique_ptrпредотвращает утечку памяти (и, следовательно, в некотором смысле выполняет свой контракт), но здесь (т. Е. С использованием режима 1) это может вызвать (при определенных обстоятельствах) что-то, что можно считать еще более вредным а именно потерю данных (уничтожение указанного значения), которых можно было бы избежать с помощью режима 3.
Марк ван Леувен

4

Да, если вы берете unique_ptrзначение by в конструкторе. Простота это хорошая вещь. Поскольку он не unique_ptrможет быть скопирован (частная копия ctor), то, что вы написали, должно привести к ошибке компиляции.


3

Изменить: Этот ответ является неправильным, хотя, строго говоря, код работает. Я оставляю это здесь только потому, что обсуждение под ним слишком полезно. Этот другой ответ является лучшим ответом, данным во время моего последнего редактирования: Как передать аргумент unique_ptr в конструктор или функцию?

Основная идея ::std::moveзаключается в том, что люди, которые передают вас, unique_ptrдолжны использовать это, чтобы выразить знание, что они знают, что unique_ptrони передают, потеряют право собственности.

Это означает, что вы должны использовать unique_ptrв своих методах ссылку на rvalue , а не на unique_ptrсебя. Это не сработает в любом случае, потому что для передачи простого старого unique_ptrпотребуется сделать копию, а это явно запрещено в интерфейсе для unique_ptr. Интересно, что использование именованной ссылки на rvalue снова превращает ее в lvalue, поэтому вам также необходимо использовать ее ::std::move в своих методах.

Это означает, что ваши два метода должны выглядеть так:

Base(Base::UPtr &&n) : next(::std::move(n)) {} // Spaces for readability

void setNext(Base::UPtr &&n) { next = ::std::move(n); }

Тогда люди, использующие методы, сделают это:

Base::UPtr objptr{ new Base; }
Base::UPtr objptr2{ new Base; }
Base fred(::std::move(objptr)); // objptr now loses ownership
fred.setNext(::std::move(objptr2)); // objptr2 now loses ownership

Как видите, ::std::moveвыражение указывает на то, что указатель потеряет право собственности в точке, где его наиболее важно и полезно знать. Если бы это произошло незаметно, людям, использующим ваш класс, было бы очень objptrстранно внезапно потерять право собственности без очевидной причины.


2
Именованные ссылки на rvalue являются lvalues.
Р. Мартиньо Фернандес

ты уверен что это Base fred(::std::move(objptr));и нет Base::UPtr fred(::std::move(objptr));?
codablank1

1
Чтобы добавить к моему предыдущему комментарию: этот код не будет компилироваться. Вам все еще нужно использовать std::moveв реализации как конструктор, так и метод. И даже когда вы передаете по значению, вызывающая std::moveсторона все равно должна использовать для передачи lvalues. Основное отличие состоит в том, что с передачей по значению этот интерфейс дает понять, что право собственности будет потеряно. Смотрите Николь Болас комментарий к другому ответу.
Р. Мартиньо Фернандес

@ codablank1: да. Я демонстрирую, как использовать конструктор и методы в базе, которые принимают rvalue ссылки.
Всезнающий

@ R.MartinhoFernandes: Ох, интересно. Я полагаю, это имеет смысл. Я ожидал, что ты ошибаешься, но реальное тестирование показало, что ты прав. Исправлено сейчас.
Всезнающий

0
Base(Base::UPtr n):next(std::move(n)) {}

должно быть намного лучше, как

Base(Base::UPtr&& n):next(std::forward<Base::UPtr>(n)) {}

а также

void setNext(Base::UPtr n)

должно быть

void setNext(Base::UPtr&& n)

с тем же телом.

И ... что evtв handle()??


3
Там нет никакой выгоды в использовании std::forwardздесь: Base::UPtr&&это всегда Rvalue ссылочный тип, и std::moveпередает его в качестве RValue. Это уже отправлено правильно.
Р. Мартиньо Фернандес

7
Я категорически не согласен. Если функция принимает unique_ptrзначение по значению, то вам гарантируется, что конструктор перемещения был вызван для нового значения (или просто, что вы получили временное значение). Это гарантирует, что unique_ptrпеременная, имеющаяся у пользователя, теперь пуста . Если вы возьмете его &&вместо этого, он будет очищен только в том случае, если ваш код вызывает операцию перемещения. По-вашему, переменная, из которой пользователь не должен был быть перемещен, возможно. Что делает использование пользователя std::moveподозрительным и запутанным. Использование std::moveдолжно всегда гарантировать, что что-то было перемещено .
Никол Болас

@NicolBolas: Ты прав. Я удалю свой ответ, потому что пока он работает, ваши наблюдения абсолютно верны.
Всезнающий

0

На верх проголосовал ответ. Я предпочитаю проходить по ссылке.

Я понимаю, что может вызывать проблема передачи по ссылке. Но давайте разделим эту проблему на две стороны:

  • для звонящего:

Я должен написать код Base newBase(std::move(<lvalue>))или Base newBase(<rvalue>).

  • для звонящего:

Автор библиотеки должен гарантировать, что он на самом деле переместит unique_ptr для инициализации члена, если он хочет владеть владельцем.

Это все.

Если вы передадите по ссылке rvalue, она вызовет только одну инструкцию «move», но если передается по значению, это два.

Да, если автор библиотеки не является экспертом в этом, он не может переместить unique_ptr для инициализации члена, но это проблема автора, а не вас. Что бы он ни передавал по значению или по ссылке на rvalue, ваш код одинаков!

Если вы пишете библиотеку, теперь вы знаете, что должны это гарантировать, так что просто сделайте это, передавая ссылку по rvalue - лучший выбор, чем значение. Клиент, который использует вашу библиотеку, просто напишет тот же код.

Теперь по вашему вопросу. Как передать аргумент unique_ptr конструктору или функции?

Вы знаете, что является лучшим выбором.

http://scottmeyers.blogspot.com/2014/07/should-move-only-types-ever-be-passed.html

Используя наш сайт, вы подтверждаете, что прочитали и поняли нашу Политику в отношении файлов cookie и Политику конфиденциальности.
Licensed under cc by-sa 3.0 with attribution required.