Как правильно писать перегрузки R-значения для операторов

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

struct Vector {
    int x, y;
    Vector() : Vector(0,0) {}
    Vector(int x, int y) : x(x), y(y) {}
};

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

Vector& operator+=(Vector const& v) {
    x += v.x;
    y += v.y;
    return *this;
}
Vector operator+(Vector const& v) const {
    return Vector(*this) += v;
}
Vector& operator-=(Vector const& v) {
    x -= v.x;
    y -= v.y;
    return *this;
}
Vector operator-(Vector const& v) const {
    return Vector(*this) -= v;
}

Однако этот код может допускать неудачные построения:

int main() {
    Vector & a = Vector(1,2) += Vector(5,4);//This compiles and invokes undefined behavior!
    std::cout << a.x << ',' << a.y << std::endl;//This isn't safe!
}

Итак, я переписал код, чтобы помнить, является ли объект L-значением или R-значением:

Vector& operator+=(Vector const& v) & {
    x += v.x;
    y += v.y;
    return *this;
}
Vector&& operator+=(Vector const& v) && {
    return std::move(*this += v);
}
Vector operator+(Vector const& v) const {
    return Vector(*this) += v;
}
Vector& operator-=(Vector const& v) & {
    x -= v.x;
    y -= v.y;
    return *this;
}
Vector&& operator-=(Vector const& v) && {
    return std::move(*this -= v);
}
Vector operator-(Vector const& v) const {
    return Vector(*this) -= v;
}

Итак, мой оставшийся вопрос заключается в том, что, хотя этот код компилируется и выполняет то, что я ожидаю, этот код безопасен и не содержит неожиданных Undefined Поведение?

int main() {
    //No Longer compiles, good.
    //Vector & a = Vector(1,2) += Vector(5,4);

    //Is this safe?
    Vector b = Vector(1,2) += Vector(5,4);

    //Other cases where this code could be unsafe?
}

Ответ 1

Вот относительно стандартные способы перегрузки этих операторов:

Vector& operator+=(Vector const& v)& {
  x += v.x;
  y += v.y;
  return *this;
}
friend Vector operator+(Vector lhs, Vector const& v) {
  lhs+=v;
  return std::move(lhs); // move is redundant yet harmless in this case
}
Vector& operator-=(Vector const& v)& {
  x -= v.x;
  y -= v.y;
  return *this;
}
friend Vector operator-(Vector lhs, Vector const& v) {
  lhs -= v;
  return std::move(lhs); // move is redundant yet harmless in this case
}

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

a+b+c становится (a+b)+c, а возвращаемое значение a+b направляется непосредственно в аргумент lhs +c. И первая строка вашего + заключалась в создании копии так или иначе, поэтому дополнительная копия в подписи безобидна.

Если у вас нет веской причины, запретите += и = по rvalues. int не поддерживает его, вы тоже не должны.

Ответ 2

Если вы сомневаетесь, делайте это как int.

Можете ли вы выполнить сложное назначение на int rvalues? Конечно нет. Итак, зачем беспокоиться о своем Vector?


Ваш b безопасен, но Vector&& c = Vector(1,2) += Vector(5,4); нет. Обычным исправить является возврат по значению, но возврат по значению из оператора присваивания также несколько странный.

Ответ 3

Перемещение - плохая идея.

Vector&& operator-=(Vector const& v) && {
    return std::move(*this -= v);
}

Вы можете легко завершить перемещенный объект, не ожидая этого.


В основном:

int main() {
    Vector & a = Vector(1,2) += Vector(5,4);//This compiles and invokes undefined behavior!
    std::cout << a.x << ',' << a.y << std::endl;//This isn't safe!
}

Почему бы просто не использовать

Vector a = Vector(1,2) + Vector(5,4)

Что касается правильного использования ссылок r-значения в перегрузках операторов, вы можете использовать их только в +, а не в + =. См. std::string operator + и operator + =