От операций с динамическими контейнерами до констант времени компиляции — C++ предлагает много интересных техник (как в этом знаменитом меме :)). В этой статье мы рассмотрим несколько продвинутых методов инициализации: от reserve() и emplace_back для контейнеров, до piecewise_construct и forward_as_tuple для кортежей. Благодаря этим техникам мы можем уменьшить количество временных объектов и более эффективно создавать переменные. Давайте приступим! В качестве основы для наших изысканий мы будем использовать следующий класс. Он хорошо подходит для иллюстрации того, когда вызываются его специальные функции-члены. Таким образом, мы сможем отследить все временные объекты.    Этот тип я позаимствовал из другой своей статьи: Moved or Not Moved - That Is the Question! - C++ Stories С приготовлениями все. Начнем с относительно простого, но очень важного элемента: Векторы в C++ — это динамические массивы, которые по мере необходимости могут увеличиваться в размерах. Однако каждый раз, когда вектор вырастает за пределы своей текущей емкости, это может потребовать перераспределение памяти, что может быть весьма дорогостоящей операцией. Для оптимизации этого процесса мы можем использовать метод reserve() в сочетании с emplace_back. Метод reserve не изменяет размер вектора, но гарантирует, что под вектор будет выделено достаточно памяти для хранения заданного количества элементов. Заранее зарезервированное место позволяет предотвратить многократную реаллокацию при добавлении элементов в вектор. Давайте рассмотрим пример, в котором сравниваются эти методы:  И мы получим следующий вывод: Запустить в @Compiler Explorer Как я уже говорил, в этом примере можно увидеть сравнение трех техник вставки: только push_back() только emplace_back() reserve() с emplace_back В первом случае нам приходится передавать временные объекты в push_back – они перемещаются для инициализации элементов вектора. Но при этом происходит реаллокация, так как при добавлении второго элемента вектор должен увеличить свой размер. Техника emplace_back() немного лучше и проще в написании, так как не создается никаких временных объектов. Но наиболее эффективен третий вариант, поскольку мы можем заранее зарезервировать место, а затем просто создавать элементы по мере необходимости. Используя reserve, а затем emplace_back, мы гарантируем, что вектору не придется перераспределять память по мере добавления элементов в пределах зарезервированного объема. Эта комбинация является мощным способом оптимизации производительности, особенно при добавлении большого количества элементов в вектор. constinit — это мощный инструмент для обеспечения константной инициализации, особенно для статических или локальных переменных потока. Введенное в C++20, это ключевое слово решает давнюю проблему C++: фиаско с порядком статической инициализации. Обеспечивая инициализацию переменных во время компиляции, constinit дает нам более предсказуемый и безопасный процесс инициализации. По своей сути constinit гарантирует, что переменная, которую он квалифицирует, будет инициализирована во время компиляции. Это особенно полезно для глобальных и статических переменных, поскольку позволяет избежать проблем с порядком динамической инициализации. Рассмотрим следующий пример: В приведенном выше коде глобальная переменная инициализируется во время компиляции с помощью функции compute. Однако, в отличие от const и constexpr, constinit не делает переменную иммутабельной. Это означает, что, хотя ее начальное значение задается во время компиляции, оно может быть изменено во время выполнения, как это показано в функции main. Кроме того, поскольку переменная constinit не является constexpr, ее нельзя использовать для инициализации другого constinit-объекта (например, int another). Подробнее об этом вы можете почитать в других моих статьях: const vs constexpr vs consteval vs constinit in C++20 - C++ Stories и Solving Undefined Behavior in Factories with constinit from C++20 - C++ Stories. В C++14 лямбда-выражения претерпели существенное обновление: появилась возможность инициализировать новые члены данных непосредственно в списке захвата лямбда-выражения. Эта фича, известная как захват с инициализатором или обобщенный лямбда-захват, обеспечивает большую гибкость и точность при работе с лямбдами. Традиционно лямбда-выражения могли захватывать переменные из окружающей их области видимости. В C++14 появилась возможность создавать и инициализировать новые переменные-члены непосредственно в списке захвата, что сделало лямбды еще более универсальными. Рассмотрим следующий пример: Вывод: 42 В этом примере мы создаем новую переменную z, которая инициализируется суммой x и y. Эта инициализация происходит в момент определения лямбды, а не в момент ее вызова. В результате, даже если x и y будут изменены после определения лямбды, z сохранит свое изначальное значение. Чтобы лучше продемонстрировать эту особенность я покажу вам, как лямбда переводится в вызываемый тип: По сути, лямбда становится экземпляром неименованной структуры с методом operator()() и членом данных z. Захват с инициализатором не ограничивается простыми типами. Можно также захватывать ссылки. Когда нам может пригодиться этот прием? По крайней мере, в двух случаях: захват по значению move-able-only типов  оптимизация Рассмотрим первый сценарий; вот как можно захватить std::unique_ptr: Раньше в C++11 уникальный указатель нельзя было захватить по значению. Нам был доступен только захват по ссылке. Начиная с C++14, мы можем переместить в замыкание объект: Еще о вариантом использования может быть оптимизация: Если вы захватываете переменную, а затем вычисляете некоторый временный объект: Почему бы не вычислить его один раз и не хранить внутри лямбда-объекта: Таким образом, savedString вычисляется один раз, а не при каждом вызове функции. С появлением умных указателей мы получили инструменты, существенно снижающие риски, связанные с динамическим выделением памяти. Однако, как и в любом инструменте, здесь всегда есть место для совершенствования и оптимизации. При использовании make_unique (или make_shared) для аллокации массивов по умолчанию происходит инициализация значения каждого элемента. Это означает, что для встроенных типов каждый элемент устанавливается в ноль, а для пользовательских – вызываются их конструкторы по умолчанию. Хотя это гарантирует, что память инициализируется в известное состояние, это выливается в накладные расходы, особенно если выделенную память предполагается перезаписать немедленно. Рассмотрим следующее: Эта строка не только выделяет память под 1000 целых чисел, но и инициализирует каждое из них нулем. Если на следующем этапе эта память будет заполнена данными из файла или результата запроса, то предварительное обнуление будет лишним и бесполезным. Для устранения этой неэффективности в C++20 появились функции make_unique_for_overwrite и make_shared_for_overwrite. Эти функции выделяют память, не инициализируя ее предварительными значениями, что делает их более быстрыми в тех случаях, когда предполагается непосредственная перезапись памяти. Функции _for_overwrite следует использовать только, когда выделенная память сразу же перезаписывается другими данными. Если память не будет сразу же перезаписана, то она будет содержать неопределенные значения, что может привести к неопределенному поведению при обращении к ней. Эти новые функции могут привести к заметному повышению производительности приложений, предполагающих большие объемы операций с памятью, как, например, средства обработки данных или игровые движки. И, наконец, мы добрались до пятого приема — прямой инициализации пар или кортежей с помощью многопараметрических конструкторов. Здесь на помощь приходят std::piecewise_construct и std::forward_as_tuple. Например: Приведенный выше код создает пару без каких-либо лишних временных объектов MyType. Но как быть в случае, когда у нас имеется дополнительный конструктор, принимающий два аргумента: В этом случае попытка: не удается, так как для компилятора этот вызов неоднозначен. В таких случаях на помощь приходит std::piecewise_construct. Это тег, который указывает std::pair выполнить конструкцию по частям. В сочетании со std::forward_as_tuple, который создает кортеж из lvalue или rvalue ссылок, мы можем передавать конструкторам элементов пары сразу несколько аргументов. Если мы запустим эту программу, то увидим следующий вывод: Запустить в @Compiler Explorer Как видно, здесь мы получили два временных объекта, созданных в рамках обычного способа. С помощью опции piecewise мы можем передавать параметры элементам пары напрямую. std::piecewise_construct особенно полезен при использовании таких контейнеров, как std::map и std::unordered_map, хранящих пары ключ-значение (std::pair). Полезность std::piecewise_construct становится очевидной, когда необходимо вставить элементы в эти контейнеры, а ключ или значение (или оба) имеют многопараметрические конструкторы или являются некопируемыми. Смотрите пример ниже: В этой статье рассматриваются различные приемы инициализации языка C++. Мы погружались в сложные современные возможности C++, включая эффективность reserve и emplace_back, точность constinit и гибкость лямбда-инициализации. Кроме того, мы рассматривали тонкие возможности функций piecewise и forward_as_tuple. Эти продвинутые техники демонстрируют постоянную эволюцию и силу языка C++, давая разработчикам возможность писать более выразительный, эффективный и универсальный код. Кто-то может посчитать это ненужным усложнением языка, но я придерживаюсь другой точки зрения. Например, функция emplace(), которая может улучшить вставку элементов в контейнер. Однако если оптимизация особо не нужна, то вместо нее можно передавать временные объекты, используя более простой код. Язык C++ обеспечивает простой подход, но при этом позволяет пользователям углубляться во внутреннее устройство для создания оптимального кода, при необходимости работая "под капотом". Приведенный здесь список продвинутых техник не претендует быть исчерпывающим. Мне было бы интересно узнать о других полезных приемах инициализации объектов более эффективным и в то же время сложным способом. Пожалуйста, не стесняйтесь поделиться своими соображениями на эту тему в комментариях. Перевод статьи подготовлен для будущих студентов курса "C++ Developer. Professional". А начинающих разработчиков на плюсах приглашаем на открытый урок, на котором познакомимся с одной из самых популярных IDE — Visual Studio Code. На этой встрече с нуля настроим VS Code, соберем и отладим небольшой C++ проект, а также познакомимся с инструментами из экосистемы C++. Записаться можно на странице специализации C++.  Редактор