Является ли следующий код конструктора перемещения безопасным?
Это конструктор перемещения класса X
:
X::X(X&& rhs)
: base1(std::move(rhs))
, base2(std::move(rhs))
, mbr1(std::move(rhs.mbr1))
, mbr2(std::move(rhs.mbr2))
{ }
Вот о чем я опасаюсь:
- Мы переходим от
rhs
дважды, а rhs
не может находиться в допустимом состоянии. Разве это не поведение undefined для инициализации base2
?
- Мы перемещаем соответствующие члены
rhs
в mbr1
и mbr2
, но поскольку rhs
уже перемещен из (и, опять же, он не гарантированно находится в допустимом состоянии), почему это должно работать?
Это не мой код. Я нашел его на сайте. Является ли это конструктором перемещения безопасным? И если да, то как?
Ответы
Ответ 1
Это примерно так, как обычно работает конструктор неявного перемещения: каждый подобъект базы и элемента создается из соответствующего подобъекта rhs
.
Предполагая, что base1
и base2
являются основами X
, у которых нет конструкторов, принимающих X
/X&
/X&&
/const X&
, это безопасно, как написано. std::move(rhs)
будет неявно преобразовываться в base1&&
(соответственно base2&&
) при передаче в инициализаторы базового класса.
EDIT: предположение фактически укусило меня пару раз, когда у меня был конструктор шаблонов в базовом классе, который точно соответствовал X&&
. Было бы безопаснее (хотя и невероятно педантично) явно выполнять преобразования:
X::X(X&& rhs)
: base1(std::move(static_cast<base1&>(rhs)))
, base2(std::move(static_cast<base2&>(rhs)))
, mbr1(std::move(rhs.mbr1))
, mbr2(std::move(rhs.mbr2))
{}
или даже просто:
X::X(X&& rhs)
: base1(static_cast<base1&&>(rhs))
, base2(static_cast<base2&&>(rhs))
, mbr1(std::move(rhs.mbr1))
, mbr2(std::move(rhs.mbr2))
{}
который, я считаю, должен точно воспроизвести то, что компилятор будет генерировать неявно для X(X&&) = default;
, если нет других базовых классов или членов, чем base1
/base2
/mbr1
/mbr2
.
ИЗМЕНИТЬ СНОВА: С++ 11 § 12.8/15 описывает точную структуру неявных конструкторов копирования/перемещения по элементам.
Ответ 2
Это зависит от иерархии наследования. Но шансы очень хорошие, что этот код в порядке. Вот полная демонстрация, показывающая, что она безопасна (для этой конкретной демонстрации):
#include <iostream>
struct base1
{
base1() = default;
base1(base1&&) {std::cout << "base1(base1&&)\n";}
};
struct base2
{
base2() = default;
base2(base2&&) {std::cout << "base2(base2&&)\n";}
};
struct br1
{
br1() = default;
br1(br1&&) {std::cout << "br1(br1&&)\n";}
};
struct br2
{
br2() = default;
br2(br2&&) {std::cout << "br2(br2&&)\n";}
};
struct X
: public base1
, public base2
{
br1 mbr1;
br2 mbr2;
public:
X() = default;
X(X&& rhs)
: base1(std::move(rhs))
, base2(std::move(rhs))
, mbr1(std::move(rhs.mbr1))
, mbr2(std::move(rhs.mbr2))
{ }
};
int
main()
{
X x1;
X x2 = std::move(x1);
}
который должен выводиться:
base1(base1&&)
base2(base2&&)
br1(br1&&)
br2(br2&&)
Здесь вы видите, что каждая база и каждый элемент перемещаются ровно один раз.
Помните: std::move
не работает. Это просто актерский состав, не более того.
Таким образом, код передает значение rvalue X
, а затем передает это значение базовым классам. Предполагая, что базовые классы выглядят так, как я изложил выше, тогда есть неявный приведение в rvalue base1
и base2
, которые будут перемещать конструкцию этих двух отдельных баз.
Кроме того,
Помните: перемещенный объект находится в действительном, но неуказанном состоянии.
Пока конструктор перемещения base1
и base2
не попадает в производный класс и не изменяет mbr1
или mbr2
, то эти члены все еще находятся в известном состоянии и готовы к перемещению из. Нет проблем.
Теперь я упомянул, что могут возникнуть проблемы. Вот как это делается:
#include <iostream>
struct base1
{
base1() = default;
base1(base1&& b)
{std::cout << "base1(base1&&)\n";}
template <class T>
base1(T&& t)
{std::cout << "move from X\n";}
};
struct base2
{
base2() = default;
base2(base2&& b)
{std::cout << "base2(base2&&)\n";}
};
struct br1
{
br1() = default;
br1(br1&&) {std::cout << "br1(br1&&)\n";}
};
struct br2
{
br2() = default;
br2(br2&&) {std::cout << "br2(br2&&)\n";}
};
struct X
: public base1
, public base2
{
br1 mbr1;
br2 mbr2;
public:
X() = default;
X(X&& rhs)
: base1(std::move(rhs))
, base2(std::move(rhs))
, mbr1(std::move(rhs.mbr1))
, mbr2(std::move(rhs.mbr2))
{ }
};
int
main()
{
X x1;
X x2 = std::move(x1);
}
В этом примере base1
имеет шаблонный конструктор, который принимает значение rvalue-something. Если этот конструктор может привязываться к rvalue X
, и если этот конструктор переместится из rvalue X
, , то, у вас возникнут проблемы:
move from X
base2(base2&&)
br1(br1&&)
br2(br2&&)
Способ исправления этой проблемы (что относительно редко, но не исчезает редко), forward<base1>(rhs)
вместо move(rhs)
:
X(X&& rhs)
: base1(std::forward<base1>(rhs))
, base2(std::move(rhs))
, mbr1(std::move(rhs.mbr1))
, mbr2(std::move(rhs.mbr2))
{ }
Теперь base1
видит rvalue base1
вместо rvalue X
и будет привязываться к конструктору перемещения base1
(если он существует), и поэтому вы снова получаете:
base1(base1&&)
base2(base2&&)
br1(br1&&)
br2(br2&&)
И все снова хорошо с миром.
Ответ 3
Нет, это будет пример поведения возможного undefined. Это может работать много времени, но это не гарантируется. С++ позволяет это, но вы тот, кто должен убедиться, что вы не пытаетесь использовать rhs
снова таким образом. Вы пытаетесь использовать rhs
три раза после , чтобы его кишки были вырваны ссылка rvalue
была передана функции, которая может потенциально перейти от нее (оставив ее в undefined).