Вот укороченная версия примера, на который я натолкнулся сегодня:
#include <vector>
#include <cassert>
using namespace std;
struct node{
vector<node> children;
} root;
int main()
{
root.children.push_back(node());
root.children[0].children.push_back(node());
assert(root.children[0].children.size() == 1); //OK now
root = root.children[0]; //My foot is hurt! My foot is very much hurt!
assert(root.children.size() == 1); //Assertion failed!
return 0;
}
Почему так происходит? Потому, что при присваивании разрушается присваиваемый объект. Граждане, будьте бдительны!
Правильно было:
#include <vector>
#include <cassert>
using namespace std;
struct node{
vector<node> children;
} root;
int main()
{
root.children.push_back(node());
root.children[0].children.push_back(node());
assert(root.children[0].children.size() == 1); //OK now
root = node(root.children[0]); //the copy constructor overcomes the problem
assert(root.children.size() == 1); //OK now
return 0;
}
Думаете какая асимптотическая сложность у этого кода?
Там перелокация памяти происходит когда реальный размер достигает резервируемый, тогда вызываются деструкторы всех членов контейнера. Можно посчитать сколько раз вызывается деструктор если, написать в него что то типа: Akhoo_Kilani(){ for (int i = 0; i < n; i++); static int c = 0; c++; cout << c << endl;}
Еще можно так сделать: struct node{ vector children; void operator = (const node &n2) { node tmp = n2; children = tmp.children; } } root;
Да, оператор присваивания из теории работает, спасибо!
Много лет назад я прострелил себе ногу похожим образом. Я писал дп на DAG, а вычисленные результаты хранил в хеш-мапе самописной, где память выделялась вектором. в итоге получался код навроде
функция DP могла в свою очередь позвать
GetIndexFromHashTable
которая могла изменить размерhashHeapVector
вызвав при этом ресайз и перевыделение памяти. компилятор MSVC сначала вызывал функцию DP, а затем вычислял ссылку на то, куда будет присвоен результатhashHeapVector[hashIndex]
и всё работало ОК. Оптимизирующий гнусный компилятор сначала вычислял ссылку, затем вызывал рекурсию, вектор ресайзился и вычисленная ранее ссылка начинала указывать на юг, вызывая RE при попытке записать что-то в эту память, причем только в случае если размер вектора превосходил 64к элементов.Почему это плохо? Потому что это undefined behavior. Компилятор может вычислять эти вещи в любом порядке, и предвидеть что произойдет не всегда просто. Нужно избегать таких выражений где слева и справа от оператора присваивания фигурирует один и тот же объект. Ну точнее если если справа он передан по ссылке (не константной). У меня вектор не передавался по ссылке функции DP в строчке присваивания, но это происходило неявно т.к. вектор был глобальный и функция его могла изменить.
ОК, теперь понятно.
При присваивании
root.children
чиститься.Это, конечно, старый пост, но раз уж я на него наткнулся…
Сначала я не мог понять, что не так. Я протестировал код на всех имеющихся у меня компиляторах, и он падал только на MSVC. Я был готов это назвать багом в MSVCRT, потому что никаких причин, по которым этот код должен был бы не работать, я не видел. Что там где уничтожается? Всё там прекрасно, если в правильном порядке делать действия в реализации vector. (MSVCRT сначала копирует и уничтожает элементы, а потом берёт
_Right.size()
. libstdc++ и libc++ сначала запоминают размер, а уже потом модифицируют элементы.)Параллельно я придумал другой тест, с которым дела обстояли по-другому. Давайте добавим не по одному элементу в каждый вектор, а по два. Вот так:
root = [[[], []], []]
. Тогда при выполненииroot = root[0]
рекурсивно сначала присвоитсяroot[0] = root[0][0]
, который пустой — и элементыroot[0]
будут уничтожены. А потом, выйдя из рекурсии, будет попытка присвоитьroot[1] = root[0][1]
. Ноroot[0][1]
к этому моменту уничтожен! При тестировании никаких конкретных багов я не наблюдал, но действия производились именно в этом порядке и понятно, что использование объекта после его уничтожения ничего хорошего не сулит.Но я никак не мог понять: если это не должно работать, то где стандарт это говорит, либо если стандарт это нигде не говорит, баг ли это в реализации библиотек и как его избежать?
Но вот я нашёл ответ. Оказывается, всё просто: определение структуры
node
само по себе некорректно*! При определении полей структуры сама структура является incomplete type, а §17.6.4.8 [res.on.functions] C++11 и C++14 гласит:И для вектора, да и вообще для любого контейнера, особого разрешения стандарт не даёт. В §17.4.3.6 [lib.res.on.functions] C++03 присутствует точно такой же текст с той лишь разницей, что существование особого разрешения для отдельных компонентов вообще даже не предусмотрено.
* Некорректно в том смысле, что вызывает неопределённые последствия. В том числе вполне может вызвать сломанные assert’ы, использование уничтоженных объектов, крэши и форматирование жёсткого диска.