Почему оператор ++ возвращает неконстантное значение?

Я прочитал "Эффективное С++ 3rd Edition", написанное Скоттом Мейерсом.

Пункт 3 книги "Использовать const, когда это возможно", говорит, что если мы хотим предотвратить случайное присвоение значения rval для возвращаемого значения функции, тип возврата должен быть const.

Например, функция приращения для iterator:

const iterator iterator::operator++(int) {
    ...
}

Затем предотвращаются некоторые несчастные случаи.

iterator it; 

// error in the following, same as primitive pointer
// I wanted to compare iterators
if (it++ = iterator()) {
  ...
}

Однако итераторы, такие как std::vector::iterator в GCC, не возвращают значения const.

vector<int> v;
v.begin()++ = v.begin();   // pass compiler check

Есть ли причины для этого?

Ответ 1

Я уверен, что это связано с тем, что он будет играть хаос с ссылками rvalue и любым типом decltype. Несмотря на то, что эти функции не были в С++ 03, они, как известно, уже приближаются.

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

Например,

auto it = ++vec.begin();

является вполне допустимым и действительно действительной семантикой, если это не совсем желательно. Рассмотрим мой класс, который предлагает цепочки методов.

class ILikeMethodChains {
public:
    int i;
    ILikeMethodChains& SetSomeInt(int param) {
        i = param;
        return *this;
    }
};
ILikeMethodChains func() { ... }
ILikeMethodChains var = func().SetSomeInt(1);

Если это запрещено, просто потому, что иногда мы можем назвать функцию, которая не имеет смысла? Нет, конечно нет. Или как насчет "swaptimization"?

std::string func() { return "Hello World!"; }
std::string s;
func().swap(s);

Это было бы незаконным, если func() создало выражение const, но оно совершенно корректно и действительно, предполагая, что реализация std::string не выделяет никакой памяти в конструкторе по умолчанию, как быстро, так и читабельно/читабельно.

Что вы должны понимать, так это то, что правила rvalue/lvalue С++ 03 откровенно просто не имеют смысла. Они, фактически, только частично испечены, и минимум, необходимый для того, чтобы запретить некоторые вопиющие ошибки, разрешая некоторые возможные права. Правила С++ 0x rvalue намного более понятны и намного более полны.

Ответ 2

Если it не const const, я ожидаю, что *(++it) предоставит мне изменяемый доступ к тому, что он представляет.

Однако разыменование итератора const дает только не изменяемый доступ к предмету, который он представляет. [ изменить: нет, это тоже неправильно. Я действительно сдаюсь!]

Это единственная причина, по которой я могу думать.

Как вы справедливо отметили, следующее неверно сформировано, потому что ++ на примитиве дает значение r (которое не может быть на LHS):

int* p = 0;
(p++)++;

Таким образом, на этом языке, кажется, есть что-то несоответствие.

Ответ 3

EDIT. Это не отвечает на вопрос, как указано в комментариях. Я просто оставлю сообщение здесь, если это так полезно...

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

Я знаю, что это может показаться противоречивым, особенно с учетом вашего примера. Но если вы думаете о большинстве случаев использования, это имеет смысл. Возьмите алгоритм STL, например std::equal. Независимо от того, является ли ваш контейнер постоянным или нет, вы всегда можете кодировать что-то вроде bool e = std::equal(c.begin(), c.end(), c2.begin()), не задумываясь о правильной версии начала и конца.

Это общий подход в STL. Помните operator[]... Имея в виду, что контейнеры должны использоваться с алгоритмами, это правдоподобно. Хотя также заметно, что в некоторых случаях вам может потребоваться определить итератор с подходящей версией (iterator или const_iterator).

Хорошо, это то, что приходит мне на ум прямо сейчас. Я не уверен, насколько это убедительно...

Боковое примечание. Правильный способ использования постоянных итераторов - это const_iterator typedef.