Каков "правильный" способ избежать слияния (например, при добавлении элемента контейнера к себе) на С++?

std::vector<int> a;
a.push_back(1);
a.push_back(a[0]);

I только что узнал, что приведенный выше код может быть очень опасным.

(Если не понятно, почему, вы не одиноки... для меня это было не очевидно).

Мои вопросы:

  • Каков "стандартный" способ борьбы с ним? Создание новой переменной и последующее присвоение ей чего-то после этого кажется мне немного странным. Есть ли лучший способ справиться с этим?

  • Как вы тренируетесь, чтобы следить за такими проблемами псевдонимов? Какие шаблоны вы ищете? Я понятия не имею, чтобы признать эту ситуацию; Я только узнал об aliasing, когда узнал о ключе restrict в C, и только теперь я понимаю, в чем проблема.


Изменить:

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

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

Есть ли какие-либо другие легкие вещи, которые можно заметить и следить за ними?

Ответ 1

РЕДАКТИРОВАТЬ. Технически стандарт не требует, чтобы это было правильно, если содержащийся тип имеет конструктор без броска. Я не знаю какой-либо реализации, где это не выполняется в любом случае, так как это потребует создания двух реализаций push_back, когда общий не менее эффективен во всех случаях.

Сглаживание - проблема вообще, но не в этом конкретном случае. Код:

assert( v.size() > 0 );
v.push_back( v[0] );

Гарантируется, что он соответствует стандарту (С++ 03) с помощью гарантий исключения (которые действительно являются хорошей причиной не для реализации ваших собственных контейнеров, вы, вероятно, не получите их права). В частности, §23.1 [lib.container.requirements]/10 dictattes:

Если не указано иное (см. 23.2.1.3 и 23.2.4.3) [ПРИМЕЧАНИЕ: обе эти ссылки относятся к insert в deque и vector соответственно], все типы контейнеров, определенные в этом разделе, удовлетворяют следующим дополнительным требованиям:

- если исключение вызывается функцией push_back() или push_front(), эта функция имеет без эффектов.

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

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

Ответ 2

Я думаю, это было бы безопасно:

std::vector<int> a(1);
a.push_back(1);
a.push_back(int(a[0]));

Ответ 3

В push_back(const T& el); реализации, чтобы проверить, находится ли el внутри массива или другого внутреннего хранилища. Это единственный политически правильный способ решения таких проблем.

Контейнер должен обрабатывать это как разные контейнеры - разные правила безопасности.

Ответ 4

Это, вероятно, не является полезным ответом для вас, но IMHO "правильный" способ заключается в том, что класс контейнера должен корректно обрабатывать псевдонимы, чтобы вызывающему абоненту не пришлось беспокоиться об этом. В частности, push_back() (или эквивалент) должен делать следующее:

// C++-ish pseudo-code, exception-safety left as an exercise for the reader
void push_back(const T & t)
{
   if (current_size == alloced_size)
   {
      // Oops, our data array is full.  Time to trade it in for a bigger one
      T * newArray = new T[alloced_size*2];
      copy_items(newArray, current_array, current_size);
      newArray[current_size++] = t;
      delete [] current_array;    // delete old array only AFTER all references to t
      current_array = new_array;
      alloced_size *= 2;
   }
   else current_array[current_size++] = t;
}

Ответ 5

Я просто прикрываю это, поэтому, пожалуйста, не считайте это Евангелием, но будет ли это работать?

a.push_back(1);
a.push_back(&(new int(a[0])));