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

(Почему) должен ли конструктор перемещения или оператор перемещения оператора очистить свой аргумент?

Пример реализации конструктора перемещения из курса С++. Im принимает немного похожее на это:

/// Move constructor
Motorcycle::Motorcycle(Motorcycle&& ori)
    : m_wheels(std::move(ori.m_wheels)),
      m_speed(ori.m_speed),
      m_direction(ori.m_direction)
{
    ori.m_wheels = array<Wheel, 2>();
    ori.m_speed      = 0.0;
    ori.m_direction  = 0.0;
}

(m_wheels является членом типа std::array<Wheel, 2>, а Колесо содержит только double m_speed и bool m_rotating. В классе Мотоцикл t25 > и m_direction также double s.)

Я не совсем понимаю, почему значения ori s должны быть очищены.

Если у Motorcycle были какие-либо элементы указателя, которые мы хотели "украсть", то обязательно, wed должен установить ori.m_thingy = nullptr, чтобы не, например, delete m_thingy дважды. Но имеет ли значение, когда поля содержат сами объекты?

Я спросил друга об этом, и они указали мне на эту страницу, в котором говорится:

Перемещение конструкторов обычно "крадет" ресурсы, удерживаемые аргументом (например, указатели на объекты с динамическим распределением, файловые дескрипторы, сокеты TCP, потоки ввода-вывода, запущенные потоки и т.д.), а не копировать их и оставлять аргумент в некотором действительном, но в противном случае неопределенном состоянии. Например, переход от a std::string или из std::vector может привести к тому, что аргумент останется пустым. Однако на это поведение нельзя полагаться.

Кто определяет, какое неопределенное состояние означает? Я не вижу, как настройка скорости до 0.0 более неопределенная, чем ее оставление. И как последнее предложение в цитате говорит - код не должен полагаться на состояние перемещенного из мотоцикла в любом случае, так зачем его очищать?

4b9b3361

Ответ 1

Их не нужно чистить. Дизайнер класса решил, что было бы неплохо оставить инициализированный исходный объект.

Обратите внимание, что ситуация, где есть смысл, - это объекты, управляющие ресурсами, которые освобождаются в деструкторе. Например, указатель на динамически выделенную память. Если оставить указатель в перемещенном объекте немодифицированным, это означает, что два объекта управляют одним и тем же ресурсом. Оба их деструктора попытаются освободить.

Ответ 2

Кто определяет, какое неопределенное состояние означает?

Автор класса.

Если вы посмотрите на документацию для std::unique_ptr, вы увидите, что после следующих строк:

std::unique_ptr<int> pi = std::make_unique<int>(5);
auto pi2 = std::move(pi);

pi фактически находится в очень определенном состоянии. Это будет reset(), а pi.get() будет равно nullptr.

Ответ 3

Скорее всего, вы правы, что автор этого класса помещает ненужные операции в конструктор.

Даже если m_wheels был распределенным по кучу типом, например std::vector, по-прежнему не было бы никакой причины его "очистить", поскольку он уже передается его собственному конструктору move:

: m_wheels(std::move(ori.m_wheels)),   // invokes move-constructor of appropriate type

Однако вы не показали достаточного количества класса, чтобы мы могли узнать, нужны ли в этом конкретном случае явные операции "очистки". Как ответ Deduplicator, ожидается, что следующие функции будут корректно вести себя в состоянии "перемещено":

// Destruction
~Motorcycle(); // This is the REALLY important one, of course
// Assignment
operator=(const Motorcycle&);
operator=(Motorcycle&&);

Следовательно, вы должны посмотреть на реализацию каждой из этих функций, чтобы определить правильность конструктора move.

Если все три используют реализацию по умолчанию (которая, из того, что вы показали, кажется разумной), тогда нет причин вручную очищать перемещенные объекты. Однако, если какая-либо из этих функций использует значения m_wheels, m_speed или m_direction, чтобы определить, как вести себя, тогда конструктор move может нуждаться чтобы очистить их, чтобы обеспечить правильное поведение.

Такой дизайн класса был бы неэлегантным и подверженным ошибкам, поскольку обычно мы не ожидали, что значения примитивов будут иметь значение в состоянии "перемещено", если только примитивы явно не используются в качестве флагов, чтобы указать, будет ли очистка должен произойти в деструкторе.

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

Ответ 4

Есть несколько вещей, которые должны быть безопасно выполнимы с перемещенным объектом:

  • Уничтожьте его.
  • Назначить/Переместить к нему.
  • Любые другие операции, которые прямо говорят об этом.

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

В качестве дополнения предпочитайте явно дефолт над пользовательскими функциями и неявно определенными над ними для краткости и сохранения тривиальности.

Ответ 5

Для этого нет стандартного поведения. Как и указатель, вы можете использовать их после удаления. Некоторые люди видят, что вам все равно, и просто не используйте повторно объект, но компилятор не запретит это.

Вот один blogpost (не я) об этом с интересной дискуссией в комментарии:

https://foonathan.github.io/blog/2016/07/23/move-safety.html

и последующие действия:

https://foonathan.github.io/blog/2016/08/24/move-default-ctor.html

А вот и недавний видеочат об этой теме с аргументами, обсуждающими это:

https://www.youtube.com/watch?v=g5RnXRGar1w

В основном это касается обработки перемещенного объекта, такого как удаленный указатель, или обеспечения его безопасности в качестве "перемещенного из состояния"

Ответ 6

Кто определяет, какое неопределенное состояние означает?

Английский язык, я думаю. "Неопределенный" здесь имеет одно из своих обычных значений для английского языка, "Не определено, не точно фиксировано в степени, неопределенное, неопределенное, не установлено Не определено или не определено" . Состояние перемещенного объекта не ограничено стандартом, отличным от того, что он должен быть "действительным" для этого типа объекта. Это не должно быть одинаковым при каждом перемещении объекта.

Если бы мы говорили только о типах, предоставляемых реализацией, то правильный язык был бы "действительным, но иначе неуказанным состоянием". Но мы оставляем слово "неуказанным" для обсуждения деталей реализации С++, а не сведений о том, какой код пользователя разрешен.

"Действительный" определяется отдельно для каждого типа. В качестве примера используются целые типы, ловушечные представления недействительны и все, что представляет значение, действительно.

Этот стандарт здесь не говорит о том, что конструктор перемещения должен сделать объект неопределенным, просто чтобы он не помещал его в какое-либо определенное состояние. Поэтому, хотя вы правы, что 0 не является "более неопределенным", чем старое значение, оно в любом случае является спорным, поскольку конструктор перемещения не должен ставить старый объект "как можно более неопределенным".

В этом случае автор класса решил поместить старый объект в одно конкретное состояние. Затем он полностью зависит от того, документируют ли они это состояние, и если они это сделают, то это полностью зависит от пользователей кода, полагаются ли они на него.

Я обычно рекомендую не полагаться на него, потому что при определенных обстоятельствах код, который вы пишете, думая о семантическом перемещении, на самом деле делает копию. Например, вы помещаете std::move в правую часть задания, не заботясь о том, является ли объект const или нет, потому что он работает в любом случае, а затем кто-то еще приходит и думает "ах, он перемещен из, он должен быть очищен до нулей". Неа. Они упустили из виду, что Motorcycle очищается при переходе с него, но const Motorcycle, конечно, нет, независимо от того, что документация может им предложить: -)

Если вы собираетесь установить состояние вообще, то это действительно монета, которая забрасывает это состояние. Вы можете установить его в "ясное" состояние, возможно, совпадающее с тем, что делает конструктор no-args (если он есть), исходя из того, что это представляет собой самое нейтральное значение. И я полагаю, что на многих архитектурах 0 - это (возможно, совместное) самое дешевое значение для установки чего-то. В качестве альтернативы вы можете установить его на какое-то значение для глазного знака, в надежде, что когда кто-то напишет ошибку, когда они случайно перейдут от объекта, а затем используют его значение, они сами подумают: "Что? Скорость света? живая улица? О да, я помню, что значение, которое этот класс устанавливает при перемещении, я, вероятно, сделал это".

Ответ 7

Исходный объект - это rvalue, потенциально значение xvalue. Поэтому нужно учитывать, что это неизбежное разрушение этого объекта.

Ручками или указателями ресурсов являются наиболее значимый элемент, который отличает перемещение от копии: после операции предполагается, что право собственности на этот дескриптор передается.

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

Thing::Thing(Thing&& rhs)
     : unqptr_(move(rhs.unqptr_))
     , rawptr_(rhs.rawptr_)
     , ownsraw_(rhs.ownsraw_)
{
    the.ownsraw_ = false;
}

Обратите внимание, что я не очищаю rawptr_.

Здесь принимается конструктивное решение о том, что до тех пор, пока флаг собственности действителен только для одного объекта, нам все равно, есть ли висячий указатель.

Однако другой инженер может решить, что указатель должен быть очищен, так что вместо случайного ub следующая ошибка приводит к доступу nullptr:

void MyClass::f(Thing&& t) {
    t_ = move(t);
    // ...
    cout << t;
}

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

Рассмотрим:

class Motorcycle
{
    float speed_ = 0.;
    static size_t s_inMotion = 0;
public:
    Motorcycle() = default;
    Motorcycle(Motorcycle&& rhs);
    Motorcycle& operator=(Motorcycle&& rhs);

    void setSpeed(float speed)
    {
        if (speed && !speed_)
            s_inMotion++;
        else if (!speed && speed_)
            s_inMotion--;
        speed_ = speed;
    }

    ~Motorcycle()
    {
        setSpeed(0);
    }
};

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

Оператор перемещения может использовать setSpeed для заполнения себя, или он может просто сделать

Motorcycle::Motorcycle(Motorcycle&& rhs)
    : speed_(rhs.speed_)
{
    rhs.speed_ = 0;  // without this, s_inMotion gets confused
}

(Извинения за опечатки или автокоррекции, введенные на моем телефоне)