Подтвердить что ты не робот

Странное поведение std:: map

Следующая тестовая программа

#include <map>
#include <iostream>

using namespace std;

int main(int argc, char **argv)
{
    map<int,int> a;
    a[1]=a.size();
    for(map<int,int>::const_iterator it=a.begin(); it!=a.end(); ++it)
            cout << "first " << (*it).first << " second " << (*it).second << endl;
}

приводит к разному выходу при компиляции на g++ 4.8.1 (Ubuntu 12.04 LTS):

g++ xxx.cpp 
./a.out 
first 1 second 1

и на Visual Studio 2012 (Windows 7) (стандартный проект приложений консоли Win32):

ConsoleApplication1.exe
first 1 second 0

Какой компилятор прав? Я что-то делаю неправильно?

4b9b3361

Ответ 1

Это действительно хорошо сформированная программа, которая имеет два одинаково допустимых пути выполнения, поэтому оба компилятора правы.

a[1] = a.size()

В этом выражении оценка двух операндов = не имеет последствий.

§1.9/15 [intro.execution] За исключением тех случаев, когда отмечено, оценки операндов отдельных операторов и подвыражений отдельных выражений не имеют последствий.

Однако вызовы функций не чередуются, поэтому вызовы operator[] и size на самом деле неопределенно секвенированы, а не не подвержены последовательности.

§1.9/15 [intro.execution] Каждая оценка в вызывающей функции (включая другие вызовы функций), которая иначе не определенно секвенирована до или после выполнения тела вызываемой функции, неопределенно упорядочена относительно выполнения вызываемой функции.

Это означает, что вызовы функций могут выполняться в одном из двух порядков:

  • operator[], затем size
  • size, затем operator[]

Если ключ не существует и вы вызываете operator[] с этим ключом, он будет добавлен к карте, тем самым изменив размер карты. Таким образом, в первом случае ключ будет добавлен, размер будет получен (теперь он равен 1), а 1 будет назначен этому ключу. Во втором случае будет получен размер (который равен 0), ключ будет добавлен, а 0 будет назначен этому ключу.

Обратите внимание, что это не та ситуация, которая вызывает поведение undefined. undefined поведение происходит, когда две модификации или модификация и чтение одного и того же скалярного объекта не подвержены.

§1.9/15 [intro.execution] Если побочный эффект скалярного объекта не влияет на какой-либо другой побочный эффект на одном и том же скалярном объекте или вычисление значения с использованием значения одного и того же скалярного объекта, поведение не определено.

В этой ситуации они не являются необъективными, но неопределенно упорядочены.

Итак, у нас есть два одинаково допустимых порядка выполнения программы. Либо это может произойти, и оба дают достоверный результат. Это неуказанное поведение.

§1.3.25 [defns.unspecified]
неспецифицированное поведение
поведение, для хорошо сформированной конструкции программы и правильных данных, которая зависит от реализации


Итак, чтобы ответить на ваши вопросы:

Какой компилятор прав?

Оба они.

Я что-то делаю неправильно?

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

auto size = a.size();
a[1] = size; // value is 0

Или:

a[1];
a[1] = a.size(); // value is 1

Если вы хотите, чтобы результат был 1, и вы знаете, что ключ еще не существует, вы, конечно, можете сделать первый код, но назначьте size + 1.

Ответ 2

В этом случае, где a[1] возвращает примитивный тип, см. этот ответ. В случае, когда тип значения std::map является определяемым пользователем типом и operator=(T, std::size_t) определен для этого типа, выражение:

a[1] = a.size();

может быть преобразован в версию с меньшим синтаксическим сахаром:

a[1] = a.size();
a.operator[](1) = a.size();
operator=(a.operator[](1), a.size());

И, как мы все знаем из §8.3.6/9:

Порядок оценки аргументов функции не указан.

что приводит к тому, что результат вышеупомянутого выражения неуточнен.

Мы имеем, разумеется, два случая:

  • Если сначала оценивается a.operator[](1), размер карты увеличивается на 1, что приводит к первому результату (first 1 second 1).
  • Если сначала оценивается a.size(), вывод, который вы получите, является вторым (first 1 second 0).

Ответ 3

Это называется проблемой sequence-point, что означает, что определенные операции могут выполняться в любом порядке, выбранном компилятором.

Если у кого-то есть побочные эффекты на другом, он называется "неопределенным поведением", немного похожим на "поведение undefined", однако, когда результат должен быть одним из фиксированного подмножества результатов, так что здесь он должен быть либо 0 или 1 и не может быть другого значения. В действительности вы обычно должны избегать этого.

В вашем конкретном случае. выполнение operator [] на карте изменяет свой размер (если этот элемент еще не существует). Таким образом, он имеет побочный эффект в правой части того, что он ему присваивает.