Перегрузка константной версии оператора [] для шаблонного класса

Еще один вопрос по перегрузке оператора [] в C ++, в частности его const версии.

Согласно странице cppreference о перегрузке оператора, при перегрузке оператора индексации массива

struct T
{
          value_t& operator[](std::size_t idx)       { return mVector[idx]; }
    const value_t& operator[](std::size_t idx) const { return mVector[idx]; }
};

Если известно, что тип значения является встроенным, вариант const должен возвращаться по значению.

Итак, если value_t является встроенным типом, вариант const должен выглядеть

    const value_t operator[](std::size_t idx) const { return mVector[idx]; }

или возможно даже

   value_t operator[](std::size_t idx) const { return mVector[idx]; }

поскольку квалификатор const не очень полезен для такого возвращаемого значения.


Теперь у меня есть шаблон class T (чтобы сохранить то же имя, что и в ссылке), который используется как со встроенными типами данных, так и с пользовательскими типами, некоторые из которых могут быть тяжелыми.

template<class VT>
struct T
{
          VT& operator[](std::size_t idx)       { return mVector[idx]; }
    const VT& operator[](std::size_t idx) const { return mVector[idx]; }
};

Согласно приведенному выше совету, я должен использовать enable_if с некоторыми type_traits, чтобы различать между экземплярами шаблонных классов со встроенными / не встроенными типами.

Я должен это делать? Эта рекомендация только для того, чтобы избежать потенциального ненужного разыменования для встроенных типов, или за ней скрывается что-то еще, о чем следует знать?


Примечания:

  • этот класс активно участвует в горячей части кода, экземпляры которого создаются как встроенными, так и настраиваемыми типами.
  • код используется кроссплатформенным с несколькими компиляторами с разной степенью оптимизации.
  • таким образом, я заинтересован в том, чтобы сделать его правильным и портативным, а также во избежание любого потенциального ущерба для производительности.
  • Мне не удалось найти никаких дополнительных рассуждений в стандарте C ++, но чтение standardeze не является моей самой сильной стороной.

Существующие вопросы по StackOverflow:


person Anton Menshov    schedule 17.12.2019    source источник
comment
Я не вижу причин не возвращаться всегда по ссылке. Почему фундаментальный value_t должен вести себя иначе?   -  person walnut    schedule 17.12.2019
comment
@walnut потенциальное ненужное разыменование для встроенных типов, возможно (?) предотвращение исключения копирования, и два авторитетных источника рекомендуют иное. - ›Итак, мой вопрос, так как мне интересно, действительно ли я должен различать встроенные и не встроенные типы, используя enable_if   -  person Anton Menshov    schedule 17.12.2019


Ответы (2)


Я не согласен с приведенным выше «советом». Учти это:

T t = /*Initialize `t`*/;
const T::value_t &vr = std::as_const(t)[0];
const auto test = vr; //Copy the value
t[0] = /*some value other than the original one.*/
assert(test != vr);

Срабатывает ли assert? Он не должен срабатывать, потому что мы просто ссылаемся на значение в контейнере. По сути, std::as_const(t)[i] должен иметь тот же эффект, что и std::as_const(t[i]). Но этого не произойдет, если ваша const версия возвращает значение. Таким образом, такое изменение коренным образом меняет семантику кода.

Поэтому, даже если вы знаете, что value_t является фундаментальным типом, вы все равно должны возвращать const&.

Обратите внимание, что диапазоны C ++ 20 официально распознают диапазоны, которые не возвращают фактические value_type& из своих operator* или эквивалентных функций. Но даже в этом случае такие вещи являются фундаментальной частью природы этого диапазона, а не свойством, которое изменяется в зависимости от параметра шаблона (см. vector<bool>, почему это плохая идея).

person Nicol Bolas    schedule 17.12.2019

Вам не нужно обрабатывать фундаментальные типы особым образом. Просто всегда возвращайте value_t& для варианта, отличного от const, и const value_t& для варианта const.

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

Я не могу думать ни о какой другой причине по-другому обрабатывать фундаментальные типы.


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

Если вы возвращаете ссылку const на элемент, пользователь может сохранить эту ссылку и наблюдать за изменениями в элементе контейнера (до тех пор, пока не произойдет аннулирование ссылки определенным образом). Если вы вернете значение, увидеть изменения невозможно.

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

Кроме того, даже если вы возвращаете по значению для основных типов, перегрузка, возвращающая по значению, будет вызываться только через const ссылки на объект. В большинстве случаев у пользователя, вероятно, есть экземпляр вашего контейнера, отличный от const, и для использования этой потенциальной оптимизации ему потребуется явным образом привести ссылку на объект к const перед вызовом перегрузки оператора.

Поэтому, если оптимизация вызывает беспокойство, я бы предпочел добавить дополнительную функцию-член, которая всегда возвращает копию элемента контейнера по значению и которая может использоваться пользователем, если потенциальное разыменование определяется как проблема производительности. . Вызов этой функции-члена не вызовет больше проблем, чем вызов правильной перегрузки оператора.

person walnut    schedule 17.12.2019