Я кодирую небольшую библиотеку числового анализа в 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 плохой вещью здесь? Спасибо заранее.
Вы должны предпочесть перегрузку операторов как свободных функций для получения симметрии полного типа (одинаковые преобразования могут применяться с левой и правой стороны). Это делает немного более очевидным, что вы упускаете из вопроса. Восстановление вашего оператора в качестве бесплатных функций, которые вы предлагаете:
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+
эффективен при любых обстоятельствах.
Чтобы опираться на то, что сказали другие:
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;
}
Наконец, вас также может заинтересовать метод, предложенный Борис Колпаков а также Сумант Тамбе а также Скотта Мейерса ответ к идее.
Я согласен с Дэвидом Родригесом, что было бы лучше использовать не-член 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, что дешевле, чем копирование или перемещение.std::move
к этому. Это превращает параметр в r-значение, что позволяет компилятору перемещаться с него. Если вы просто возвращаете параметр, компилятор должен выполнить копирование возвращаемого значения.&&
«параметр выведенного типа, который может быть ссылкой на rvalue или ссылкой на lvalue), применить std::forward
к этому. Без этого компилятор должен выполнить копирование в возвращаемое значение. С его помощью компилятор может выполнить перемещение, если ссылка связана с rvalue.