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

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

Решение

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

(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

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) По значению: если вы имеете в виду, что функция претендует на владение 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 сделать хранение.

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

Если указатель гарантированно не равен нулю (а это не так, поскольку списки могут быть пустыми), можно предпочесть передать ссылку, а не указатель. Это может быть указатель / ссылка на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 (ссылаясь на временную ссылку). с) (В отличие от противоположной ситуации std::move, бросок из Y&& в Y&, с Y тип интеллектуального указателя не возможен; тем не менее, это преобразование может быть получено с помощью простой шаблонной функции, если это действительно необходимо; см. /questions/40100798/est-li-privedenie-ili-standartnaya-funktsiya-s-effektom-protivopolozhnyim-stdmove/40100816#40100816). В случае, когда вызываемая функция намеревается безоговорочно получить право собственности на объект, украдя у аргумента, обязательство предоставить аргумент 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;
}

Эта функция может быть вызвана как в 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> именующий 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) вместо этого они могли бы гарантировать, что старый код работает без изменений, потому что (в отличие от уникальных указателей) автоматические указатели могут быть автоматически разыменованы со значением (сам объект указателя в процессе сбрасывается до нуля). Очевидно, комитет так сильно предпочел пропагандировать режим 3, а не режим 1, что он решил активно нарушать существующий код, а не использовать режим 1 даже для уже устаревшего использования.

Когда предпочитать режим 3, а не режим 1

Режим 1 идеально подходит для использования во многих случаях и может быть предпочтительнее, чем режим 3, в тех случаях, когда принятие владения в противном случае принимает форму перемещения интеллектуального указателя на локальную переменную, как в reversed пример выше. Однако я вижу две причины предпочесть режим 3 в более общем случае:

  • Несколько эффективнее передать ссылку, чем создать временный указатель и убрать старый указатель (обработка наличности несколько трудоемка); в некоторых сценариях указатель может быть передан несколько раз без изменений в другую функцию, прежде чем он будет фактически украден. Такое прохождение обычно требует написания std::move (если не используется режим 2), но обратите внимание, что это просто приведение, которое фактически ничего не делает (в частности, не разыменовывает), поэтому к нему добавлена ​​нулевая стоимость.

  • Если это возможно, что что-либо создает исключение между началом вызова функции и точкой, в которой оно (или некоторый содержащийся в нем вызов) фактически перемещает объект, на который указывает указатель, в другую структуру данных (и это исключение еще не перехватывается внутри самой функции).), то при использовании режима 1 объект, на который указывает умный указатель, будет уничтожен до catch Предложение может обрабатывать исключение (поскольку параметр функции был уничтожен при разматывании стека), но не при использовании режима 3. Последнее дает вызывающей стороне возможность восстановить данные объекта в таких случаях (путем перехвата исключения). Обратите внимание, что режим 1 здесь не вызывает утечку памяти, но может привести к невосстановимой потере данных для программы, что также может быть нежелательным.

Возврат умного указателя: всегда по значению

Чтобы завершить слово о возвращении умного указателя, предположительно указывающего на объект, созданный для использования вызывающей стороной. Это не совсем случай, сравнимый с передачей указателей на функции, но для полноты я хотел бы настаивать на том, чтобы в таких случаях всегда возвращалось значениене используйте std::move в return заявление). Никто не хочет получить ссылку на указатель, который, вероятно, только что был отменен.

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

tl;dr: не использовать unique_ptrвот так.

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

  1. Только взять unique_ptr параметры конструктора, если вы опубликовали unique_ptr члены.

unique_ptrs обернуть сырые указатели для управления владением и сроком службы. Они отлично подходят для локального использования - не подходят и не предназначены для взаимодействия. Хотите интерфейс? Задокументируйте свой новый класс как переход собственности и позвольте ему получить исходный ресурс; или, возможно, в случае указателей используйтеowner<T*>как предлагается в Основных рекомендациях.

Только если цель вашего занятия - провести unique_ptrи попросите других использовать эти unique_ptrкак таковые - только тогда ваш конструктор или методы могут их использовать.

  1. Не раскрывайте тот факт, что вы используете unique_ptrвнутренне

С помощью unique_ptrдля узлов списка - это во многом деталь реализации. На самом деле, даже тот факт, что вы позволяете пользователям вашего механизма, подобного списку, просто использовать голый узел списка напрямую - создавая его самостоятельно и передавая вам - это не очень хорошая идея, ИМХО. Мне не нужно формировать новый list-node-which-is-also-a-list, чтобы добавить что-то в ваш список - я должен просто передать полезную нагрузку - по значению, по const lvalue ref и / или по rvalue ref. Тогда вы разберетесь с этим. А для списков сращивания - снова value, const lvalue и / или rvalue.

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

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

Это означает, что вы должны использовать rvalue ссылку на unique_ptr в ваших методах, а не 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 внезапно теряют право собственности без видимой причины.

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()??

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

Я понимаю, что может вызвать проблема передачи ссылки на 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

Другие вопросы по тегам