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

Перегрузка по ссылкам R-значения и дублированию кода

Рассмотрим следующее:

struct vec
{
    int v[3];

    vec() : v() {};
    vec(int x, int y, int z) : v{x,y,z} {};
    vec(const vec& that) = default;
    vec& operator=(const vec& that) = default;
    ~vec() = default;

    vec& operator+=(const vec& that)
    {
        v[0] += that.v[0];
        v[1] += that.v[1];
        v[2] += that.v[2];
        return *this;
    }
};

vec operator+(const vec& lhs, const vec& rhs)
{
    return vec(lhs.v[0] + rhs.v[0], lhs.v[1] + rhs.v[1], lhs.v[2] + rhs.v[2]);
}
vec&& operator+(vec&& lhs, const vec& rhs)
{
    return move(lhs += rhs);
}
vec&& operator+(const vec& lhs, vec&& rhs)
{
    return move(rhs += lhs);
}
vec&& operator+(vec&& lhs, vec&& rhs)
{
    return move(lhs += rhs);
}

Благодаря r-значным ссылкам, с этими четырьмя перегрузками оператора + я могу свести к минимуму количество созданных объектов, повторно используя временные. Но мне не нравится дублирование кода, которое это вводит. Могу ли я добиться такого же результата с меньшим повторением?

4b9b3361

Ответ 1

Повторное использование временных рядов - интересная идея, и вы не единственный, кто написал функции, которые возвращают ссылки rvalue по этой причине. В старшем C + + 0x проект-оператор + (string & &, string const &) также был объявлен для возврата ссылки rvalue. Но это изменилось по уважительным причинам. Я вижу три проблемы с такой перегрузкой и выбором типов возврата. Два из них не зависят от фактического типа, а третий аргумент относится к типу типа vec.

  • Вопросы безопасности. Рассмотрим такой код:

    vec a = ....;
    vec b = ....;
    vec c = ....;
    auto&& x = a+b+c;
    

    Если ваш последний оператор возвращает ссылку rvalue, x будет оборванной ссылкой. В противном случае это не произойдет. Это не искусственный пример. Например, трюк auto&& используется во внутреннем цикле внутри диапазона, чтобы избежать ненужных копий. Но так как правило продления срока службы для временных во время ссылки привязки не применяется в случае вызова функции, который просто возвращает ссылку, вы получите ссылку на свидание.

    string source1();
    string source2();
    string source3();
    
    ....
    
    int main() {
      for ( char x : source1()+source2()+source3() ) {}
    }
    

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

  • В общем коде функции, возвращающие ссылки rvalue, заставляют вас писать

    typename std::decay<decltype(a+b+c)>::type
    

    вместо

    decltype(a+b+c)
    

    просто потому, что последний op + может вернуть ссылку rvalue. По моему скромному мнению, это становится уродливым.

  • Поскольку ваш тип vec является "плоским" и "малым", эти op + перегрузки вряд ли полезны. См. Ответ FredOverflow.

Заключение: следует избегать функций с типом возвращаемого ссылочного значения rvalue, особенно если эти ссылки могут относиться к краткосрочным временным объектам. std::move и std::forward являются специальными исключениями для этого правила.

Ответ 2

Так как ваш тип vec "плоский" (внешних данных нет), перемещение и копирование делают точно то же самое. Таким образом, все ваши ссылки на rvalue и std::move получают от вас абсолютное отсутствие производительности.

Я бы избавился от всех дополнительных перегрузок и просто написал классическую версию reference-to-const:

vec operator+(const vec& lhs, const vec& rhs)
{
    return vec(lhs.v[0] + rhs.v[0], lhs.v[1] + rhs.v[1], lhs.v[2] + rhs.v[2]);
}

Если у вас мало понимания семантики перемещения, я рекомендую изучить этот вопрос.

Благодаря r-значным ссылкам, с этими четырьмя перегрузками оператора + я могу свести к минимуму количество созданных объектов, повторно используя временные ряды.

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

Ответ 3

Это, которое уже прекрасно работает в текущем С++, будет использовать семантику перемещения (если доступно) в С++ 0x. Он уже обрабатывает все случаи, но полагается на копирование elision и inlining, чтобы избежать копирования - поэтому он может делать больше копий, чем хотелось бы, особенно для второго параметра. Хороший бит об этом - это работает без каких-либо других перегрузок и делает правильную вещь (семантически):

vec operator+(vec a, vec const &b) {
  a += b;
  return a;  // "a" is local, so this is implicitly "return std::move(a)",
             // if move semantics are available for the type.
}

И здесь вы остановитесь, в 99% случаев. (Я, скорее всего, недооценил). Остальная часть этого ответа применяется только после того, как вы знаете, например, с помощью профилировщика, что дополнительные копии из op + заслуживают дальнейшей оптимизации.


Чтобы полностью избежать всех возможных копий/ходов, вам действительно понадобятся эти перегрузки:

// lvalue + lvalue
vec operator+(vec const &a, vec const &b) {
  vec x (a);
  x += b;
  return x;
}

// rvalue + lvalue
vec&& operator+(vec &&a, vec const &b) {
  a += b;
  return std::move(a);
}

// lvalue + rvalue
vec&& operator+(vec const &a, vec &&b) {
  b += a;
  return std::move(b);
}

// rvalue + rvalue, needed to disambiguate above two
vec&& operator+(vec &&a, vec &&b) {
  a += b;
  return std::move(a);
}

Вы были на правильном пути с вашим, без реального уменьшения (AFAICT), хотя, если вам нужно это часто задавать часто для многих типов, макрос или CRTP могут сгенерировать его для вас. Единственное реальное различие (мое предпочтение для отдельных вышеприведенных утверждений - незначительное) - это ваши копии, когда вы добавляете два lvalues ​​в operator + (const vec & lhs, vec && rhs):

return std::move(rhs + lhs);

Сокращение дублирования через CRTP

template<class T>
struct Addable {
  friend T operator+(T const &a, T const &b) {
    T x (a);
    x += b;
    return x;
  }

  friend T&& operator+(T &&a, T const &b) {
    a += b;
    return std::move(a);
  }

  friend T&& operator+(T const &a, T &&b) {
    b += a;
    return std::move(b);
  }

  friend T&& operator+(T &&a, T &&b) {
    a += b;
    return std::move(a);
  }
};

struct vec : Addable<vec> {
  //...
  vec& operator+=(vec const &x);
};

Теперь нет необходимости определять какой-либо op + специально для vec. Добавляемый повторно используется для любого типа с op + =.

Ответ 4

Я закодировал ответ Фреда Нурка, используя clang + libС++. Мне пришлось удалить использование синтаксиса инициализатора, потому что clang еще не реализует это. Я также поставил оператор print в конструкторе копирования, чтобы мы могли рассчитывать копии.

#include <iostream>

template<class T>
struct AddPlus {
  friend T operator+(T a, T const &b) {
    a += b;
    return a;
  }

  friend T&& operator+(T &&a, T const &b) {
    a += b;
    return std::move(a);
  }

  friend T&& operator+(T const &a, T &&b) {
    b += a;
    return std::move(b);
  }

  friend T&& operator+(T &&a, T &&b) {
    a += b;
    return std::move(a);
  }

};

struct vec
    : public AddPlus<vec>
{
    int v[3];

    vec() : v() {};
    vec(int x, int y, int z)
    {
        v[0] = x;
        v[1] = y;
        v[2] = z;
    };
    vec(const vec& that)
    {
        std::cout << "Copying\n";
        v[0] = that.v[0];
        v[1] = that.v[1];
        v[2] = that.v[2];
    }
    vec& operator=(const vec& that) = default;
    ~vec() = default;

    vec& operator+=(const vec& that)
    {
        v[0] += that.v[0];
        v[1] += that.v[1];
        v[2] += that.v[2];
        return *this;
    }
};

int main()
{
    vec v1(1, 2, 3), v2(1, 2, 3), v3(1, 2, 3), v4(1, 2, 3);
    vec v5 = v1 + v2 + v3 + v4;
}

test.cpp:66:22: error: use of overloaded operator '+' is ambiguous (with operand types 'vec' and 'vec')
    vec v5 = v1 + v2 + v3 + v4;
             ~~~~~~~ ^ ~~
test.cpp:5:12: note: candidate function
  friend T operator+(T a, T const &b) {
           ^
test.cpp:10:14: note: candidate function
  friend T&& operator+(T &&a, T const &b) {
             ^
1 error generated.

Я исправил эту ошибку следующим образом:

template<class T>
struct AddPlus {
  friend T operator+(const T& a, T const &b) {
    T x(a);
    x += b;
    return x;
  }

  friend T&& operator+(T &&a, T const &b) {
    a += b;
    return std::move(a);
  }

  friend T&& operator+(T const &a, T &&b) {
    b += a;
    return std::move(b);
  }

  friend T&& operator+(T &&a, T &&b) {
    a += b;
    return std::move(a);
  }

};

Выполнение выходов примера:

Copying
Copying

Далее я попробовал подход С++ 03:

#include <iostream>

struct vec
{
    int v[3];

    vec() : v() {};
    vec(int x, int y, int z)
    {
        v[0] = x;
        v[1] = y;
        v[2] = z;
    };
    vec(const vec& that)
    {
        std::cout << "Copying\n";
        v[0] = that.v[0];
        v[1] = that.v[1];
        v[2] = that.v[2];
    }
    vec& operator=(const vec& that) = default;
    ~vec() = default;

    vec& operator+=(const vec& that)
    {
        v[0] += that.v[0];
        v[1] += that.v[1];
        v[2] += that.v[2];
        return *this;
    }
};

vec operator+(const vec& lhs, const vec& rhs)
{
    return vec(lhs.v[0] + rhs.v[0], lhs.v[1] + rhs.v[1], lhs.v[2] + rhs.v[2]);
}

int main()
{
    vec v1(1, 2, 3), v2(1, 2, 3), v3(1, 2, 3), v4(1, 2, 3);
    vec v5 = v1 + v2 + v3 + v4;
}

Запуск этой программы не производил никакого вывода.

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