Вопрос касается перегрузки operator[], а именно различия в создании константной и неконстантной версии. В большинстве случаев это будет выглядеть так:
T& operator[](size_t i);
const T& operator[](size_t i) const;
У меня есть несколько теоретических вопросов. Практически всегда сам вызов operator[] не модифицирует объект, а значит даже неконстантную версию стоило бы объявить как const-метод.
Первый вопрос: правильно ли я понимаю, что мы не объявляем его const-методом только потому, что хотим, чтобы эти методы не конфликтовали, и чтобы при обращении к элементу некоторого константного класса вызывался второй вариант перегруженного оператора?
Второй вопрос: верно ли, что если объект будет неконстантный, для него при вызове operator[] всегда будет вызываться первая перегруженная версия?
"неконстантную версию стоило бы объявить как const-метод"
Вы пробовали? Ведь если ваш объект - константный, как вы сможете вернуть неконстантную ссылку на константный объект?
Грубо -
class Test
{
int& operator[](int i) const { return data[i]; }
private:
int data[10];
};
Ведь в этом случае data тоже становится константным.
Если бы это было возможно - тем самым была бы дыра, позволяющая законно изменять константный объект...
Практически всегда сам вызов operator[] не модифицирует объект
Зависит от семантики. Например, std::map, std::unordered_map вообще не имеют const версий этого оператора, по той причине, что вызов с несуществующим ранее ключом приводит к созданию нового элемента в контейнере (явное изменение объекта).
Пара, описанных в вопросе сигнатур (с некоторыми нюансами) используется в стандартной библиотеке для std::basic_string, std::vector, std::deque, std::bitset, std::array, std::valarray. Т.е. константная версия возвращает константную ссылку (кроме bitset, он возвращает bool по значению), а неконстантная, соответственно, обычную ссылку на элемент контейнера. У std::valarray, кстати, есть несколько пар перегрузок с разными типами параметров, но это уже другая история.
Есть и типы, имеющие только константную версию оператора. Это std::basic_string_view, std::reverse_iterator, std::move_iterator и std::match_results. Отсутствие обычной версии обусловлено необходимость доступа только-для-чтения к членам-данным объекта.
Но есть и более необычные ситуации, например, для std::unique_ptr и std::shared_ptr, когда они владеют массивами объектов. Например, в std::unique_ptr объявлена такая сигнатура:
T& operator[](size_t i) const;
Т.е. функция константная (не меняет своих членов-данных), но при этом возвращает неконстантую ссылку на объект, которым управляет указатель, и т.о. этот объект может быть изменен. Иначе бы просто сама концепция "умных" указателей была бы не так полезна.
мы не объявляем его const-методом только потому, что хотим, чтобы эти методы не конфликтовали
В частности, да. Это ограничение механизма перегрузки. Если функция останется константной, то она должна будет принимать уже какой-то другой тип в качестве параметра. Т.к. по возвращаемым типам перегрузка не работает. Но и из-за иной ситуации, уже описанной в другом ответе, когда функция константа, то и все члены-данные объекта this в этой функции константы. Чтобы можно было вернуть что-то неконстантное в этом ситуации, надо возвращать либо копию по значению, либо добавлять косвенности, как это сделано в "умных" указателях. Ведь указатель, который нельзя менять не запрещает менять данные, расположенные по адресу, хранимом в указателе.
верно ли, что если объект будет неконстантный, для него при вызове operator[] всегда будет вызываться первая перегруженная версия?
Верно, но ничто не мешает сделать const_cast приведение, добавляющее const. Можно явно, а можно и через передачу в функцию, принимающую константную ссылку. В обратную же сторону (снятие const) приведение стоит делать с большей осторожностью.
Сборка персонального компьютера от Artline: умный выбор для современных пользователей