Есть ли разница в отношении поведения undefined между итератором и скалярным объектом?

В разделе порядка оценки говорится, что следующий код приводит к поведению undefined до С++ 17:

a[i] = i++;

Это происходит из-за неуказанного порядка при оценке левой и правой частей выражения присваивания.

С++ 14 standard 1.9/15 говорит:

Если побочный эффект скалярного объекта не зависит от другого побочного эффекта для одного и того же скалярного объекта или вычисления значения с использованием значения одного и того же скалярного объекта, и они не являются потенциально параллельными (1.10), поведение undefined.

Но что, если мы используем std::vector и его объект iterator вместо скалярного объекта i?

std::vector<int> v = {1, 2};
auto it = v.begin();
*it = *it++;   // UB?

Существует ли поведение undefined (до С++ 17) или нет?

Ответ 1

В ситуациях, когда итератор является классом, поведение хорошо определено во всех версиях стандарта, предполагая, что it++ указывает на допустимое местоположение внутри его контейнера (что в вашем примере оно).

С++ переводит *it++ в эту последовательность из двух вызовов функций:

it.operator++(0).operator*();

Функциональные вызовы вводят последовательность, поэтому все побочные эффекты фактического ++, вызываемого внутри operator++ в примитиве, используемом как реализация итератора (возможно, необработанный указатель), должны быть завершены до выхода функции.

Однако итераторы не должны быть классами: они также могут быть указателями:

struct foo {
    typedef int* iterator;
    iterator begin() { return data; }
private:
    int data[10];
};

Код выглядит одинаково, и он продолжает компилироваться, но теперь поведение undefined:

foo f;
auto it = f.begin();
*it = *it++; // <<== This is UB

Вы можете предотвратить это, вызвав ++ как функцию-член:

std::vector<int> v = {1, 2};
auto it = v.begin();
*it = *it.operator++(0);

Когда итератор на самом деле является указателем, этот код не будет компилироваться, а не вызывает поведение undefined.