Передача аргументов С++ unique_ptr

Предположим, что у меня есть следующий код:

class B { /* */ };

class A {
    vector<B*> vb;
public:
    void add(B* b) { vb.push_back(b); }
};


int main() {

    A a;
    B* b(new B());
    a.add(b);
}

Предположим, что в этом случае все необработанные указатели B* можно обрабатывать через unique_ptr<B>.

Удивительно, но я не смог найти способ преобразования этого кода с помощью unique_ptr. После нескольких попыток я придумал следующий код, который компилируется:

class A {
    vector<unique_ptr<B>> vb;
public:
    void add(unique_ptr<B> b) { vb.push_back(move(b)); }
};


int main() {

    A a;
    unique_ptr<B> b(new B());
    a.add(move(b));
}

Итак, мой простой вопрос: это способ сделать это и, в частности, является move(b) единственным способом сделать это? (Я думал о ссылках на rvalue, но я не совсем понимаю их.)

И если у вас есть ссылка с полными объяснениями семантики перемещения, unique_ptr и т.д., которые я не смог найти, не стесняйтесь поделиться ею.

EDIT Согласно http://thbecker.net/articles/rvalue_references/section_01.html, мой код выглядит нормально.

Собственно, std:: move - это просто синтаксический сахар. С объектом x класса X move(x) является таким же, как:

static_cast <X&&>(x)

Эти 2 функции перемещения необходимы, поскольку кастинг на ссылку rvalue:

  • запрещает функции "добавлять" от передачи по значению
  • делает push_back использование конструктора перемещения по умолчанию для B

По-видимому, мне не нужен второй std::move в моем main(), если я изменю свою функцию "добавить" для передачи по ссылке (обычный lvalue ref).

Я хотел бы получить некоторое подтверждение всего этого, хотя...

Ответ 1

Да, вот как это должно быть сделано. Вы явно передаете право собственности с main на A. Это в основном то же, что и ваш предыдущий код, за исключением того, что он более явный и значительно более надежный.

Ответ 2

Я несколько удивлен, что на этот вопрос не ясно и ясно сказано здесь, ни на каком-либо месте, на которое я легко споткнулся. Хотя я довольно новичок в этом, я думаю, что можно сказать следующее.

Ситуация - это вызывающая функция, которая строит значение unique_ptr<T> (возможно, выбирая результат из вызова new) и хочет передать его некоторой функции, которая будет владеть объектом, на который указывает (по например, хранить его в структуре данных, как это происходит здесь, в vector). Чтобы указать, что владелец был получен владельцем, и он готов отказаться от него, передается значение unique_ptr<T>. Это насколько я вижу три разумных способа передачи такого значения.

  • Передача по значению, как в add(unique_ptr<B> b) в вопросе.
  • Передача с помощью ссылки const lvalue, как в add(unique_ptr<B>& b)
  • Передача по ссылке rvalue, как в add(unique_ptr<B>&& b)

Передача const ссылка lvalue не будет разумной, поскольку она не позволяет вызываемой функции брать собственность (и const ссылка на rvalue будет еще более глупо, чем это; я даже не уверен, что это разрешено).

Что касается действительного кода, параметры 1 и 3 почти эквивалентны: они заставляют вызывающего абонента записывать rvalue в качестве аргумента для вызова, возможно, путем обертывания переменной в вызове std::move (если аргумент уже rvalue, т.е. неназванный, как в литье из результата new, это необязательно). Однако в варианте 2 передача значения rvalue (возможно, из std::move) не допускается, и функция должна вызываться с помощью указанной переменной unique_ptr<T> (при передаче cast из new, сначала нужно назначить переменную).

Когда std::move действительно используется, переменная, содержащая значение unique_ptr<T> в вызывающем объекте, концептуально разыменовывается (преобразуется в rvalue, соответственно отбрасывается в ссылку rvalue), и право собственности на данный момент прекращается. В варианте 1. разыменование является реальным, и значение перемещается во временное, которое передается вызываемой функции (если функция calles проверила бы переменную в вызывающем, она обнаружила бы, что она уже содержит нулевой указатель). Собственность была передана, и нет возможности, чтобы вызывающий мог принять решение не принимать ее (ничего не делая с аргументом, заставляя заостренное значение быть уничтоженным при выходе функции, вызов метода release в аргументе предотвратит это, но просто приведет к утечке памяти). Удивительно, что варианты 2. и 3. семантически эквивалентны во время вызова функции, хотя для этого требуется другой синтаксис. Если вызываемая функция передаст аргумент другой функции, принимающей значение r (например, метод push_back), в обоих случаях должен быть вставлен std::move, который передаст право собственности на эту точку. Если вызываемая функция забывает что-либо сделать с аргументом, тогда вызывающий объект будет все еще владеть объектом, если он имеет имя для него (как это необходимо в варианте 2); это несмотря на тот факт, что в случае 3, поскольку прототип функции попросил абонента согласиться на освобождение собственности (путем вызова std::move или предоставления временного). Таким образом, методы do

  • Призывает вызывающего абонента отказаться от права собственности и обязательно его утвердит.
  • Принудительный вызов, чтобы обладать собственностью, и быть готовым (путем предоставления ссылки const), чтобы отказаться от нее; однако это не является явным (вызов или std::move не требуется или даже не разрешен), а также не отменяет права собственности. Я бы счел этот метод довольно неясным в своем намерении, если только явно не предполагается, что принятие права собственности или нет на усмотрение вызываемой функции (можно использовать какое-то использование, но вызывающие должны знать).
  • Призывает вызывающего абонента явно указывать отказ от владения, как в 1. (но фактическая передача права собственности задерживается до момента вызова функции).

Вариант 3 достаточно прост в своем намерении; при условии, что собственность фактически принята, это для меня лучшее решение. Это немного более эффективно, чем 1, поскольку никакие значения указателя не перемещаются во временные (вызовы std::move на самом деле просто отличные и ничего не стоят); это может быть особенно актуально, если указатель передается через несколько промежуточных функций, прежде чем его содержимое будет перемещено.

Вот несколько примеров для экспериментов.

class B
{
  unsigned long val;
public:
  B(const unsigned long& x) : val(x)
  { std::cout << "storing " << x << std::endl;}
  ~B() { std::cout << "dropping " << val << std::endl;}
};

typedef std::unique_ptr<B> B_ptr;

class A {
  std::vector<B_ptr> vb;
public:
  void add(B_ptr&& b)
  { vb.push_back(std::move(b)); } // or even better use emplace_back
};


void f() {
    A a;
    B_ptr b(new B(123)),c;
    a.add(std::move(b));
    std::cout << "---" <<std::endl;
    a.add(B_ptr(new B(4567))); // unnamed argument does not need std::move
}

Как записано, вывод

storing 123
---
storing 4567
dropping 123
dropping 4567

Обратите внимание, что значения уничтожаются в упорядоченном порядке, хранящемся в векторе. Попробуйте изменить прототип метода add (при необходимости приспособив другой код, чтобы скомпилировать его), и действительно ли он передает его аргумент b. Можно получить несколько перестановок линий вывода.

Ответ 3

Итак, мой простой вопрос: это способ сделать это и, в частности, это "движение (б)" единственный способ сделать это? (Я думал о ссылках на rvalue, но я не совсем понимаю это так...)

И если у вас есть ссылка с полными объяснениями семантики перемещения, unique_ptr..., которую я не смог найти, не стесняйтесь.

Бесстыдный плагин, найдите заголовок "Перемещение в члены". Он описывает именно ваш сценарий.

Ответ 4

Ваш код в main можно упростить, так как С++ 14:

a.add( make_unique<B>() );

где вы можете поместить аргументы для конструктора B во внутренние круглые скобки.


Вы также можете рассмотреть функцию-член класса, которая берет на себя управление необработанным указателем:

void take(B *ptr) { vb.emplace_back(ptr); }

и соответствующий код в main будет:

a.take( new B() );

Другим вариантом является использование идеальной пересылки для добавления элементов вектора:

template<typename... Args>
void emplace(Args&&... args)
{ 
    vb.emplace_back( std::make_unique<B>(std::forward<Args>(args)...) );
}

и основной код:

a.emplace();

где, как и раньше, вы можете поместить аргументы конструктора для B в круглые скобки.

Ссылка на рабочий пример