Почему перегруженные операторы присваивания перемещения возвращают ссылку lvalue вместо ссылки rvalue? - PullRequest
2 голосов
/ 11 октября 2019

Так что это действительно то, что я не могу понять семантически. Цепочка присвоений имеет смысл для семантики копирования:

int a, b, c{100};
a = b = c;

a, b и c - все это 100.

Попробуйте что-то подобное с семантикой перемещения? Просто не работает или не имеет смысла:

std::unique_ptr<int> a, b, c;
c = std::make_unique<int>(100);
a = b = std::move(c);

Это даже не компилируется, потому что a = b является назначением копирования, которое удаляется. Я мог бы привести аргумент, что после выполнения последнего выражения *a == 100 и b == nullptr и c == nullptr. Но это не гарантируется стандартом. Это не сильно изменится:

a = std::move(b) = std::move(c);

Это все еще включает в себя назначение копирования.

a = std::move(b = std::move(c));

Это на самом деле работает, но синтаксически это огромное отклонение от цепочки назначения копирования.

Чтобы объявить перегруженный оператор присваивания перемещения, нужно вернуть ссылку на lvalue:

class MyMovable
{
public:
    MyMovable& operator=(MyMovable&&) { return *this; }
};

Но почему это не ссылка на значение?

class MyMovable
{
public:
    MyMovable() = default;
    MyMovable(int value) { value_ = value; }
    MyMovable(MyMovable const&) = delete;
    MyMovable& operator=(MyMovable const&) = delete;
    MyMovable&& operator=(MyMovable&& other) {
        value_ = other.value_;
        other.value_ = 0;
        return std::move(*this);
    }
    int operator*() const { return value_; }
    int value_{};
};

int main()
{
    MyMovable a, b, c{100};
    a = b = std::move(c);

    std::cout << "a=" << *a << " b=" << *b << " c=" << *c << '\n';
}

Интересно, этот пример на самом деле работает как я ожидаю, и дает мне такой вывод:

a = 100 b = 0 c = 0

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

Итак, я поднял здесь несколько вещей, поэтому я попытаюсь сжать их в ряд вопросов. :

  1. Допустимы ли обе формы операторов присваивания?
  2. Когда вы используете один или другой?
  3. Является ли назначение перемещения цепочкой? Если да, то когда / почему вы когда-либо будете его использовать?
  4. Как вы даже цепляете задание перемещения, которое возвращает ссылки на lvalue значимым образом?

Ответы [ 4 ]

6 голосов
/ 11 октября 2019

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

Я неувидеть любую причину для возврата rvalue-ссылки в назначении перемещения. Назначение перемещения обычно выглядит так:

b = std::move(a);

и после этого b должно содержать состояние a, тогда как a будет находиться в каком-либо пустом или неопределенном состоянии.

Если вы добавите цепочку таким образом:

c = b = std::move(a);

, то вы ожидаете, что b не потеряет свое состояние, потому что вы никогда не применяли std::move к нему. Однако если ваш оператор присваивания перемещения возвращается по rvalue-reference, то это фактически переместит состояние a в b, и тогда левый оператор присваивания также вызовет присваивание перемещения, передав состояние b (который был a раньше) до c. Это удивительно, потому что теперь и a, и b имеют пустое / неопределенное состояние. Вместо этого я ожидал бы, что a будет перемещен в b и скопирован в c, что именно и происходит, если назначение перемещения возвращает lvalue-ссылку.

Теперь для

c = std::move(b = std::move(a));

это работает, как вы ожидаете, вызывая назначение перемещения в обоих случаях, и было бы ясно, что состояние b также перемещается. Но почему вы хотите это сделать? Вы могли бы перевести состояние a в c напрямую с помощью c = std::move(a); без очистки состояния b в процессе (или, что еще хуже, перевести его в состояние, которое нельзя использовать напрямую). Даже если вы этого захотите, это будет более понятно изложено в виде последовательности

c = std::move(a);
b.clear(); // or something similar

Что касается

c = std::move(b) = std::move(a)

, то, по крайней мере, ясно, что b перемещен, но, похоже,как если бы текущее состояние b было перемещено, а не после правого хода и снова, двойной ход является избыточным. Как вы заметили, это все еще вызывает назначение копирования для левой руки, но если вы действительно хотите сделать этот вызов назначением перемещения в обоих случаях, вы можете вернуться по rvalue-reference и избежать проблемы с c = b = std::move(a), описанной выше, вам нужно будет дифференцировать категорию значений среднего выражения. Это можно сделать, например, следующим образом:

MyMovable& operator=(MyMovable&& other) & {
    ...
    return *this;
}
MyMovable&& operator=(MyMovable&& other) && {
    return std::move(operator=(std::move(other)));
}

Спецификаторы & и && означают, что следует использовать конкретную перегрузку, если выражение, для которого вызывается функция-член, является lvalue или rvalue.

Я не знаю, есть ли какой-нибудь случай, который вы бы на самом деле хотели сделать это.

5 голосов
/ 11 октября 2019
  1. Допустимы ли обе формы операторов присваивания?

Они правильно сформированы и имеют четко определенное поведение. Но возвращение ссылки на rvalue на *this из функции, которая не квалифицирована как rvalue, было бы нетрадиционным и, вероятно, не очень хорошим дизайном.

Было бы очень удивительно, если бы:

a = b = std::move(c));

вызвал перемещение b из. Удивление не очень хорошая функция для API.

Когда вы будете использовать одну или другую?

В общем, вы никогда не захотите возвращать ссылку на rvalue на *this, кроме как из функции, квалифицированной как rvalue. Из квалифицированной функции rvalue предпочтительно возвращать ссылку на rvalue или, возможно, даже prvalue в зависимости от контекста.

Является ли назначение заданий цепочкой? Если да, то когда / почему вы когда-либо будете его использовать?

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

Как вы даже цепочку присваивания перемещения, которая возвращает lvalue ссылки

Как вы показали, с std::move s.

значимым образом?

Я не уверен, есть ли способ придать смысл этому.

3 голосов
/ 11 октября 2019
  1. Допустимы ли обе формы операторов присваивания?

Оператор присваивания является в основном обычным методом и не требует возврата lvalue-ссылки на тип self, возвращая void, char также будет допустимым.

Чтобы избежать неожиданности, мы пытаемся имитировать встроенное и поэтому, чтобы разрешить цепочечное присваивание, мы возвращаем ссылку lvalue на тип self.

Когда бы вы использовали один или другой?

Лично я бы использовал только MyMovable& operator=(MyMovable const&)

Является ли назначение заданий цепочкой? Если да, то когда и почему вы когда-либо будете его использовать?

Я так не думаю.

, но, чтобы разрешить синтаксис a = std::move(b) = std::move(c);, вы могли бы сделать:

    MyMovable& operator=(MyMovable const&) = delete;
    MyMovable&& operator=(MyMovable&& other) &&;
    MyMovable& operator=(MyMovable&& other) &;
1 голос
/ 11 октября 2019

Допустимы ли обе формы операторов присваивания?

В соответствии со стандартом, я верю, но будьте очень осторожны. Когда вы возвращаете ссылку на rvalue из operator=, вы говорите, что она может быть свободно изменена. Это может легко привести к неожиданному поведению. Например,

void foo(const MyMovable& m) { }
void foo(MyMovable&& m) {
    m.value_ = 666; // I'm allowed to do whatever I want to m
}

int main() {
    MyMovable d;
    foo(d = MyMovable{ 200 }); // will call foo(MyMovable&&) even though d is an lvalue!
    std::cout << "d=" << *d << '\n'; // outputs 666
}

Правильный способ исправить это, вероятно, будет определить ваш operator= следующим образом:

MyMovable&& operator=(MyMovable&& other) && {...

Теперь это работает, только если * это уже значениеи приведенный выше пример не скомпилируется, так как он использует operator= для lvalue. Тем не менее, это не позволяет вашим операторам перемещения по цепочке работать. Я не уверен, как разрешить обоим операторам перемещения цепочки, в то же время защищаясь от поведения, как в моем примере выше.

Когда бы вы использовали один или другой?

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

Является ли цепочка назначений движений чем-то важным? Если так, когда / почему вы когда-либо будете его использовать?

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

Как вы даже назначаете цепочку перемещения, которая возвращает ссылки на lvalue восмысленный способ?

Если бы мне пришлось это сделать, я бы использовал форму, которую вы использовали выше: a = std::move(b = std::move(c)); Это явно и очевидно.

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