Чем make_shared отличается от shared_ptr, созданного через new?
Ответ: При std::make_shared<T>(args...) объект и служебный блок управления обычно размещаются в одном выделенном блоке памяти. При std::shared_ptr<T>(new T(args...)) память под объект выделяется отдельно от блока управления.
Объединённое выделение уменьшает накладные расходы. Но формулировка «make_shared всегда делает ровно одно выделение» слишком категорична: стандарт рекомендует реализации обходиться не более чем одним.
Есть и нюанс с weak_ptr. Объект уничтожается, когда исчезает последний сильный владелец. Однако при объединённом размещении память общего блока может оставаться занятой, пока существуют слабые указатели. Это не означает, что сам объект продолжает жить.
Ещё одно отличие: конструктору shared_ptr можно передать собственную функцию удаления. У make_shared такого параметра нет.
Какие требования должен выполнять пользовательский ключ std::unordered_map?
Ответ: Для ключа нужны согласованные хеш-функция и проверка эквивалентности.
По умолчанию контейнер использует std::hash<Key> и std::equal_to<Key>. Для собственного типа можно передать свои функциональные объекты в параметрах шаблона: специализировать std::hash необязательно.
Главное правило: если два ключа считаются эквивалентными, их хеши обязаны совпадать. Обратное неверно — одинаковые хеши у разных ключей допустимы, это коллизия.
Например, если строки сравниваются без учёта регистра, хеширование тоже должно игнорировать регистр. Иначе равные для контейнера ключи могут попасть в разные корзины.
Проверка эквивалентности должна быть согласованной, а результаты сравнения и хеширования ключей — оставаться стабильными, пока ключи находятся в контейнере.
Когда push_back делает итераторы std::vector недействительными?
Ответ: Если при добавлении произошла реаллокация, недействительными становятся все прежние итераторы, указатели и ссылки на элементы, а также старый end().
Реаллокация требуется, когда новый размер превышает прежнюю ёмкость capacity(). Вектор получает другое хранилище, поэтому сохранённые адреса элементов больше использовать нельзя.
Если свободной ёмкости хватило, итераторы, указатели и ссылки на уже существовавшие элементы сохраняются. Однако старый end() всё равно становится недействительным: конец последовательности изменился.
Поэтому ответ «push_back всегда ломает итераторы» неверен. Но и проверять только указатель на первый элемент недостаточно: сохранённый end() требует отдельного внимания.
Ответ: Безопасность совместного владения не означает безопасность любых действий с указателем и объектом.
Разные экземпляры shared_ptr, которые владеют одним объектом, можно копировать, сбрасывать и уничтожать в разных потоках: управление общим счётчиком владельцев согласовано библиотекой.
Но если несколько потоков обращаются к одному и тому же экземпляру shared_ptr и хотя бы один меняет его, нужна синхронизация. Например, нельзя без неё выполнять reset() в одном потоке и копировать этот же указатель в другом. С C++20 для такого сценария есть std::atomic<std::shared_ptr<T>>.
Отдельный вопрос — сам объект T. Умный указатель не защищает его поля от гонки данных: для совместного изменения объекта нужны собственные меры синхронизации.
Можно ли передать std::unique_ptr в функцию по значению?
Ответ: Да, но именованный std::unique_ptr нужно передать с перемещением: consume(std::move(ptr)).
Копирование у unique_ptr запрещено. При передаче по значению параметр функции получает владение объектом, а исходный указатель после успешного перемещения становится пустым. Сам объект при этом не обязан перемещаться в памяти: меняется его владелец.
Важно: один лишь вызов std::move(ptr) ничего не передаёт. Он позволяет выбрать перемещающий конструктор, который и выполняет передачу владения.
Если функция должна только поработать с объектом, обычно достаточно передать T& или const T&. Принимать unique_ptr по значению стоит, когда функция действительно забирает владение.