Ответ 1
Да, вот как это должно быть сделано. Вы явно передаете право собственности с main
на A
. Это в основном то же, что и ваш предыдущий код, за исключением того, что он более явный и значительно более надежный.
Предположим, что у меня есть следующий код:
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).
Я хотел бы получить некоторое подтверждение всего этого, хотя...
Да, вот как это должно быть сделано. Вы явно передаете право собственности с main
на A
. Это в основном то же, что и ваш предыдущий код, за исключением того, что он более явный и значительно более надежный.
Я несколько удивлен, что на этот вопрос не ясно и ясно сказано здесь, ни на каком-либо месте, на которое я легко споткнулся. Хотя я довольно новичок в этом, я думаю, что можно сказать следующее.
Ситуация - это вызывающая функция, которая строит значение unique_ptr<T>
(возможно, выбирая результат из вызова new
) и хочет передать его некоторой функции, которая будет владеть объектом, на который указывает (по например, хранить его в структуре данных, как это происходит здесь, в vector
). Чтобы указать, что владелец был получен владельцем, и он готов отказаться от него, передается значение unique_ptr<T>
. Это насколько я вижу три разумных способа передачи такого значения.
add(unique_ptr<B> b)
в вопросе.const
lvalue, как в add(unique_ptr<B>& b)
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
не требуется или даже не разрешен), а также не отменяет права собственности. Я бы счел этот метод довольно неясным в своем намерении, если только явно не предполагается, что принятие права собственности или нет на усмотрение вызываемой функции (можно использовать какое-то использование, но вызывающие должны знать).Вариант 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
. Можно получить несколько перестановок линий вывода.
Итак, мой простой вопрос: это способ сделать это и, в частности, это "движение (б)" единственный способ сделать это? (Я думал о ссылках на rvalue, но я не совсем понимаю это так...)
И если у вас есть ссылка с полными объяснениями семантики перемещения, unique_ptr..., которую я не смог найти, не стесняйтесь.
Бесстыдный плагин, найдите заголовок "Перемещение в члены". Он описывает именно ваш сценарий.
Ваш код в 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
в круглые скобки.