Почему в Qt контейнеры можно передавать по значению без накладных расходов
Что проверяют
Знаете ли вы про неявное совместное использование в Qt и понимаете ли, когда оно перестаёт быть бесплатным. Ответ в духе «там же перемещение» сразу показывает, что механизм не изучали.
Как отвечать
Контейнеры Qt, такие как QList, QString, QByteArray, QMap, построены на копировании при записи. Сам объект это небольшой хендл с указателем на разделяемые данные, в которых лежит атомарный счётчик ссылок. Копирование увеличивает счётчик и стоит одинаково для пустого списка и для миллиона элементов, а память остаётся одна на всех владельцев.
Данные разъезжаются в момент, который называют отцеплением: как только вызван неконстантный метод, а счётчик больше единицы, контейнер делает себе глубокую копию и уменьшает счётчик у общих данных. Цена копирования не исчезает, она откладывается до первой записи и платится только тем, кто пишет.
Практические следствия:
- передавать по значению нормально, но константная ссылка всё равно предпочтительнее: она не трогает атомарный счётчик, а это межпоточная операция;
- копию можно безопасно отдать в другой поток, потому что счётчик атомарный, но менять один и тот же экземпляр из двух потоков нельзя;
- размер на выбор стратегии не влияет: никакого порога и никакой оптимизации маленького буфера здесь нет.
В Qt 6 QVector стал псевдонимом QList, а разделяемое хранение сохранилось.
Где спотыкаются
Обходят неконстантный контейнер в цикле по диапазону. Такой цикл зовёт неконстантный begin() и тем самым отцепляет данные, то есть копирует всё содержимое, хотя менять никто ничего не собирался. Для чтения контейнер оборачивают в std::as_const.
Второе: держат итератор и правят контейнер. Любая запись может отцепить данные или перевыделить буфер, после чего итератор смотрит в чужую память. Правило то же, что в стандартной библиотеке, просто здесь его нарушают чаще, ведь копия выглядит бесплатной.
Третье: раздают копии по объектам и ждут, что правку увидят все. Копирование при записи это оптимизация чтения, а не общее состояние: первая же запись отделит одну копию, остальные останутся со старыми данными.