Перегрузка ссылок на R-значения и дублирование кода - PullRequest
15 голосов
/ 15 мая 2011

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

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-значения с этими четырьмя перегрузками оператора + я могу минимизировать количество создаваемых объектов, повторно используя временные файлы. Но мне не нравится дублирование кода, которое это вводит. Могу ли я добиться того же с меньшим количеством повторений?

Ответы [ 4 ]

32 голосов
/ 17 мая 2011

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

  1. Вопросы безопасности.Рассмотрим код, подобный следующему:

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

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

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

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

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

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

    вместо

    decltype(a+b+c)
    

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

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

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

8 голосов
/ 15 мая 2011

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

Я бы избавился от всех дополнительных перегрузок и просто написал бы классическую версию с ссылкой на 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, и вы можете получить неприятные проблемы с временным временем жизни объекта. Не делай этого.

7 голосов
/ 15 мая 2011

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

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), хотя, если вам часто нужен этот op + для многих типов, макрос или CRTP может сгенерировать его для вас.Единственная реальная разница (мое предпочтение для отдельных приведенных выше утверждений незначительно) состоит в том, что вы делаете копии, когда добавляете два lvalue в 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);
};

Теперь больше нет необходимости определять какие-либо операции + специально для vec.Возможность добавления можно использовать для любого типа с оп + =.

1 голос
/ 15 мая 2011

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

#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

Далее я попробовал подход на C ++ 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;
}

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

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

...