Как передать уникальный аргумент 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(...));

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

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

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

(B) по ссылке L-значения non-const

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) по ссылке const l-value

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)) {}

это более или менее идентично случаю "не-const L-value reference". Различий два вещи.

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

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

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

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

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

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

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

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

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


позвольте мне попытаться указать различные жизнеспособные режимы передачи указателей вокруг объектов, память которых управляется экземпляром std::unique_ptr шаблон класса; он также применяется к старшему std::auto_ptr шаблон класса (который, я считаю, позволяет всем использовать этот уникальный указатель, но для которого, кроме того, будут приняты изменяемые lvalues, где ожидаются rvalues, без необходимости вызывать 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, но определение может быть изменено на use auto_ptr или shared_ptr вместо этого без особой необходимости изменять то, что сказано ниже (в частности, в отношении безопасности исключений, гарантированной без необходимости писать деструкторы).

режимы передачи смарт-указателей вокруг

Mode 0: передайте указатель или ссылочный аргумент вместо интеллектуального указателя

если ваша функция не связана с владением, это предпочтительный метод: не заставляйте ее брать умный указатель вообще. В этом деле вашей функции не нужно беспокоиться кто владеет объектом, на который указывает, или каким образом управляется владение, поэтому передача необработанного указателя является как совершенно безопасной, так и наиболее гибкой формой, поскольку независимо от владения клиент всегда может произвести необработанный указатель (либо путем вызова get метод или из адреса-оператора&).

например, функция для вычисления длины такого списка не должна давать list аргумент, но a сырой указатель:

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

интересным случаем, который попадает в категорию mode 0, является создание (глубокой) копии списка; в то время как функция, выполняющая это, должна, конечно, передать право собственности на созданную копию, она не связана с правом собственности на список, который она копирует. Таким образом, его можно определить следующим образом:

list copy(const node* p)
{ return list( p==nullptr ? nullptr : new node{p->entry,copy(p->next.get())} ); }

этот код заслуживает пристального внимания, как для вопроса о том, почему он компилирует вообще (результат рекурсивного вызова copy в списке инициализатора связывается с аргументом ссылки rvalue в конструкторе перемещения unique_ptr<node>, a.к. a. list, при инициализации существует допустимый вариант использования для режима 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) еще не могли быть украдены, так как это было бы сделано для создания


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


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

основная идея ::std::move это люди, которые передают вам 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 внезапно потерять право собственности без очевидной причины.


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.

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

  • для абонента:

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

  • для абонента:

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

вот и все.

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

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

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

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

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

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