Подтвердить что ты не робот

Является ли следующий код конструктора перемещения безопасным?

Это конструктор перемещения класса 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 уже перемещен из (и, опять же, он не гарантированно находится в допустимом состоянии), почему это должно работать?

Это не мой код. Я нашел его на сайте. Является ли это конструктором перемещения безопасным? И если да, то как?

4b9b3361

Ответ 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).