Семантика перемещения и передача по R-значению в перегруженной арифметике

Я кодирую небольшую библиотеку числового анализа в C ++. Я пытался реализовать с использованием новейших функций C ++ 11, включая семантику перемещения. Я понимаю обсуждение и топ-ответ в следующем посте: C ++ 11 значения и путаница в семантике перемещения (оператор return) , но есть один сценарий, который я все еще пытаюсь обернуть вокруг.

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

T (const T &) { /*initialization via copy*/; }
T (T &&) { /*initialization via move*/; }

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

T a, b, c, d, e;
T f = a + b * c - d / e;

Без семантики перемещения мои операторы каждый раз создают новую локальную переменную, используя конструктор копирования, так что всего получается 4 копии. Я надеялся, что с семантикой ходов я смогу уменьшить это до 2 копий плюс несколько ходов. В скобках версия:

T f = a + (b * c) - (d / e);

каждый из (b * c) а также (d / e) Я должен создать временное хранилище обычным способом с копией, но тогда было бы здорово, если бы я мог использовать один из этих временных сотрудников для накопления оставшихся результатов только с помощью ходов.

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

Вот пример реализации для оператора сложения:

T operator+ (T const& x) const
{
T result(*this);
// logic to perform addition here using result as the target
return std::move(result);
}
T operator+ (T&& x) const
{
// logic to perform addition here using x as the target
return std::move(x);
}

Без звонков std::moveтогда только const & версия каждого оператора когда-либо вызывается. Но при использовании std::move как указано выше, последующая арифметика (после самых внутренних выражений) выполняется с использованием && версия каждого оператора.

Я знаю, что RVO может быть заблокирован, но при очень сложных вычислительно-реальных проблемах кажется, что выигрыш немного перевешивает отсутствие RVO. То есть, за миллионы вычислений я получаю очень маленькое ускорение, когда я включаю std::move, Хотя, честно говоря, это достаточно быстро без. Я действительно просто хочу полностью понять семантику здесь.

Есть ли добрый C ++ Guru, который хочет найти время, чтобы объяснить простым способом, является ли и почему мое использование std :: move плохой вещью здесь? Спасибо заранее.

12

Решение

Вы должны предпочесть перегрузку операторов как свободных функций для получения симметрии полного типа (одинаковые преобразования могут применяться с левой и правой стороны). Это делает немного более очевидным, что вы упускаете из вопроса. Восстановление вашего оператора в качестве бесплатных функций, которые вы предлагаете:

T operator+( T const &, T const & );
T operator+( T const &, T&& );

Но вы не в состоянии предоставить версию, которая обрабатывает левую часть как временную:

T operator+( T&&, T const& );

И чтобы избежать двусмысленности в коде, когда оба аргумента являются значениями r, необходимо обеспечить еще одну перегрузку:

T operator+( T&&, T&& );

Общий совет будет заключаться в реализации += в качестве метода-члена, который изменяет текущий объект, а затем записать operator+ как экспедитор, который изменяет соответствующий объект в интерфейсе.

Я на самом деле не так много думал, но может быть альтернатива T (без ссылки на r / lvalue), но я боюсь, что это не уменьшит количество перегрузок, которые вы должны предоставить для выполнения operator+ эффективен при любых обстоятельствах.

8

Другие решения

Чтобы опираться на то, что сказали другие:

  • Призыв к std::move в T::operator+( T const & ) является ненужным и может помешать RVO.
  • Было бы предпочтительно предоставить не-член operator+ что делегирует T::operator+=( T const & ),

Я также хотел бы добавить, что идеальная переадресация может быть использована для сокращения числа не членов operator+ требуются перегрузки:

template< typename L, typename R >
typename std::enable_if<
std::is_convertible< L, T >::value &&
std::is_convertible< R, T >::value,
T >::type operator+( L && l, R && r )
{
T result( std::forward< L >( l ) );
result += r;
return result;
}

Для некоторых операторов этой «универсальной» версии было бы достаточно, но поскольку сложение обычно является коммутативным, мы, вероятно, хотели бы определить, когда правый операнд является r-значением, и изменить его, а не перемещать / копировать левый операнд. Это требует одной версии для правых операндов, которые являются lvalues:

template< typename L, typename R >
typename std::enable_if<
std::is_convertible< L, T >::value &&
std::is_convertible< R, T >::value &&
std::is_lvalue_reference< R&& >::value,
T >::type operator+( L && l, R && r )
{
T result( std::forward< L >( l ) );
result += r;
return result;
}

И еще один для правых операндов, которые являются значениями:

template< typename L, typename R >
typename std::enable_if<
std::is_convertible< L, T >::value &&
std::is_convertible< R, T >::value &&
std::is_rvalue_reference< R&& >::value,
T >::type operator+( L && l, R && r )
{
T result( std::move( r ) );
result += l;
return result;
}

Наконец, вас также может заинтересовать метод, предложенный Борис Колпаков а также Сумант Тамбе а также Скотта Мейерса ответ к идее.

5

Я согласен с Дэвидом Родригесом, что было бы лучше использовать не-член operator+ функции, но я отложу это в сторону и сосредоточусь на вашем вопросе.

Я удивлен, что вы видите снижение производительности при написании

T operator+(const T&)
{
T result(*this);
return result;
}

вместо

T operator+(const T&)
{
T result(*this);
return std::move(result);
}

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

В общем, правила для такого рода вещей предполагают, что у вас есть функция, возвращающая объект (т.е. не ссылка):

  • Если вы возвращаете локальный объект или параметр по значению, не применяйте std::move к этому. Это позволяет компилятору выполнять RVO, что дешевле, чем копирование или перемещение.
  • Если вы возвращаете параметр типа rvalue reference, примените std::move к этому. Это превращает параметр в r-значение, что позволяет компилятору перемещаться с него. Если вы просто возвращаете параметр, компилятор должен выполнить копирование возвращаемого значения.
  • Если вы возвращаете параметр, который является универсальной ссылкой (то есть, «&&«параметр выведенного типа, который может быть ссылкой на rvalue или ссылкой на lvalue), применить std::forward к этому. Без этого компилятор должен выполнить копирование в возвращаемое значение. С его помощью компилятор может выполнить перемещение, если ссылка связана с rvalue.
3
По вопросам рекламы [email protected]