Избегайте универсальных ссылок в правильной реализации статьи Скотта Мейера «Более эффективный C++», пункт 22. Рассмотрите возможность использования op= вместо автономной операции?

Я пытаюсь следовать совету Скотта Мейерса в пункте 22 книги «Более эффективный C++»: «Рассмотрите возможность использования op= вместо автономного op». Он предлагает создать шаблон для operator+, чтобы все классы, реализующие operator+=, автоматически получали operator+:

template<class T>
const T operator+(const T& lhs, const T& rhs)
{ 
    return T(lhs) += rhs; 
}

Теперь, в статье 25 книги «Эффективный современный C++», есть пример (стр. 172) сложения матриц, где предлагаются перегрузки operator+ со значениями r, потому что, если вы знаете, что lhs или rhs являются значениями r, вы можете использовать их для хранения результате, предотвращая бесполезные копии, возможно, огромных матриц. Поэтому я добавил перегрузки:

template<class T>
T operator+(T&& lhs, const T& rhs)
{ 
    return std::move(lhs += rhs);
}

template<class T>
T operator+(T const& lhs, T&& rhs)
{ 
    return std::move(rhs += lhs);
}

template<class T>
T operator+(T&& lhs, T&& rhs)
{ 
    return std::move(lhs += rhs);
}

Проблема здесь в том, что T&& является универсальной ссылкой, и в конечном итоге она захватывает все, поэтому я в конечном итоге перехожу от lvalues, что является нежелательным поведением.

Итак, как я могу правильно реализовать шаблон operator+?

Частичное решение с использованием передачи по значению: я также прочитал пункт 41: "Рассмотрите возможность передачи по значению для копируемых параметров..." из Effective Modern C++, поэтому я попытался написать свою собственную версию следующим образом:

template<class T>
const T operator-(T lhs, T rhs)
{
    return lhs -= rhs;
}

Но это упускает возможность оптимизации, когда rhs является значением r, так как в этом случае я не буду использовать rhs для сохранения результата. Так что это только частичное решение.


person becko    schedule 24.07.2015    source источник
comment
Вы всегда можете использовать что-то вроде Boost.Operators при реализации классов или даже std::rel_ops, если это вас не устраивает.   -  person chris    schedule 24.07.2015
comment
Можете ли вы привести пример того, как это портит арифметику итератора? Это должна быть наименее предпочтительная перегрузка operator-, и я не могу воспроизвести.   -  person Barry    schedule 24.07.2015
comment
@Барри. Спасибо за минимальный пример. Оказывается, я получаю волнистые линии от CLion, но он компилируется! Так что это действительно проблема с редактором CLion. Любая идея, что может происходить?   -  person becko    schedule 24.07.2015
comment
@Barry Полностью переписал вопрос, чтобы решить другую, но связанную проблему (без вреда, так как у меня не было ответов).   -  person becko    schedule 24.07.2015
comment
Н.Б. rhs += lhs предполагает, что сложение является коммутативным, что неверно, например, для std::string   -  person Jonathan Wakely    schedule 24.07.2015
comment
@JonathanWakely Хотите опубликовать свой ответ?   -  person Barry    schedule 24.07.2015
comment
@JonathanWakely Это не проблема. Я рассматриваю только случаи, когда сложение коммутативно.   -  person becko    schedule 24.07.2015


Ответы (1)


Я думаю, ваша проблема в том, что вы пытаетесь объединить совет С++ 03 (напишите общий operator+) с хорошим советом С++ 11 (перегрузите operator+ для значений r), и эти два совета не обязательно совместимы.

Общий operator+, который вы хотите написать, сложно правильно сделать с переадресацией ссылок, но вы можете следовать тому же подходу, что и оригинал, и создать временный переход от lhs, если это значение r, с помощью std::forward ("универсальные ссылки" теперь известные как переадресация ссылок, и это должно дать вам представление о том, что лучший способ справиться с ними обычно std::forward, а не std::move):

template<class T>
    T operator+(T&& lhs, const T& rhs)
    {
        T tmp{ std::forward<T>(lhs) };
        tmp += rhs;
        return tmp;
    }

Обратите внимание, что это будет только принимать rvalue с левой стороны, потому что другой параметр означает, что T не будет выводиться как ссылочный тип, такой как X&, а только как тип объекта, такой как X, поэтому первый параметр может соответствовать только rvalue.

Таким образом, мы можем добавить еще одну перегрузку для случая, когда левая часть представляет собой lvalue:

template<class T>
    T operator+(const T& lhs, const T& rhs)
    {
        T tmp{lhs};
        tmp += rhs;
        return tmp;
    }

Это может быть не совсем оптимальным, потому что ни одна из перегрузок не будет повторно использовать rhs, когда это значение r, но, как я сказал в комментарии выше, rhs += lhs не является правильным, если сложение не является коммутативным. Если вы готовы предположить, что он коммутирует, вы можете улучшить это, чтобы повторно использовать rhs, когда это значение r (добавляя больше перегрузок и усложняя).

Лично я думаю, что общий operator+, определенный в терминах operator+=, интересен как упражнение, но на самом деле не очень практичен, особенно если принять во внимание семантику ходов. Написание пользовательского operator+ для вашего типа (например, на стр. 172 книги More Effective C++) не составляет такого труда, и вам не нужно иметь дело с переадресацией ссылок, и вы будете знать, Кроме того, коммутирует для этого типа и безопасно ли делать rhs += lhs или нет.

person Jonathan Wakely    schedule 24.07.2015
comment
Перегрузка с rhs rvalue сложна. Я не уверен, как это сделать. Простое добавление T operator+(const T& lhs, T&& rhs) приводит к неоднозначности перегрузки всякий раз, когда lhs и rhs являются оба значениями r. Как поступить в таком случае? (Помните, вы можете предположить, что + коммутативно) - person becko; 24.07.2015
comment
Вы можете просто использовать std::move вместо std::forward<T>(lhs) в T operator+(T&& lhs, const T& rhs), поскольку, как вы объясняете, здесь lhs является rvalue, а не универсальной ссылкой. - person becko; 24.07.2015