Как передать аргумент unique_ptr в конструктор или функцию? - PullRequest
363 голосов
/ 13 ноября 2011

Я новичок в перемещении семантики в 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?

Ответы [ 6 ]

773 голосов
/ 14 ноября 2011

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

(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&&, который является ссылкой r-значения на nextBase.Это все, что он делает.

Поскольку Base::Base(std::unique_ptr<Base> n) принимает свой аргумент по значению, а не по r-значению, 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-value

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

Это более или менее идентично "неконстантной ссылкой на l-значение "case.Различия - это две вещи.

  1. Вы можете пройти временный:

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

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

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

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

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

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

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

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

50 голосов
/ 26 июня 2014

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

4 голосов
/ 14 ноября 2011

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

3 голосов
/ 14 ноября 2011

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

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

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

Это означает, что ваши два метода должны выглядеть следующим образомthis:

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 внезапно потерял право собственности без очевидной причины.

0 голосов
/ 11 мая 2018

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

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

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

Я должен написать код 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

0 голосов
/ 14 ноября 2011
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() ??

...