что такое список инициализации

C++/Список инициализации

Список инициализации — концепция суть которой состоит в том, что структура/класс или массив могут быть созданы передачей списка аргументов в порядке, соответствующем порядку определения членов структуры.

Списки инициализации рекурсивны, что позволяет их использовать для массивов структур и структур, содержащих вложенные структуры. Концепция списков инициализации пришла в C++ из C.

Содержание

Виды списков инициализации

Список инициализации это общее название концепции, которая включает в себя несколько видов и практик списков инициализации.

Инициализаторы агрегатов или списки инициализации

Это инициализация объекта посредством присвоения или передачи его конструктору по-умолчанию массива данных, то есть данных в фигурных скобках.

При таком синтаксисе данные передаются конструктору по-умолчанию, который поймёт синтаксис списка инициализации и инициализирует данные, то есть для этого не надо писать свой конструктор.

Только агрегирующие типы и POD (Plain Old Data) типы могут быть инициализированы с помощью инициализаторов агрегатов (вида SomeType var = <1,2,3>;).

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

Cписок инициализации и шаблонный класс std::initializer_list

Данное описание позволяет создать SequenceClass из последовательности целых чисел следующим образом:

Здесь демонстрируется работа особого вида конструктора для списка инициализации. Классы, содержащие подобные конструкторы, обрабатываются особым образом во время инициализации.

Класс std::initializer_list<> определён в стандартной библиотеке C++11. Однако, объекты данного класса могут быть созданы компилятором C++11 только статически с использованием синтаксиса со скобками <>. Список может быть скопирован после создания, однако, это будет копированием по ссылке. Список инициализации является константным: ни его члены, ни их данные не могут быть изменены после создания, поэтому наиболее правильно будет выглядеть так:

Так как std::initializer_list<> является полноценным типом, он может быть использован не только в конструкторах. Обычные функции могут получать типизированные списки инициализации в качестве аргумента, например:

Стандартные контейнеры могут быть инициализированы следующим образом:

Универсальная инициализация или расширение синтаксиса списков инициализации

Такой подход это изменённый синтаксис конструктора. Требует создания вручную конструктора со специфическим синтаксисом.

Такой синтаксис «: x_(x), y_(y)» это универсальная инициализация или расширение синтаксиса списков инициализации. Такой подход является безотказным, универсальным (подходит и работает везде), практичным (экономит память при инициализации сложных типов) и рекомендуемым.

В таком списке инициализации переменная сразу создаётся и инициализируется. Это позволяет инициализировать константы и сложные типы без вызова их конструкторов, что невозможно при инициализации внутри тела конструктора, когда переменные уже созданы и происходит только их настройка.

В случае классов/структур такой вид списка инициализации применим только к конструкторам.

Универсальная инициализация не заменяет полностью синтаксиса инициализации с помощью конструктора. Если в классе есть конструктор, принимающий в качестве аргумента список инициализации (ИмяТипа(initializer_list );), он будет иметь более высокий приоритет по сравнению с другими возможностями создания объектов. Например, в C++11 std::vector содержит конструктор, принимающий в качестве аргумента список инициализации:

Данный код приведёт к вызову конструктора, принимающего в качестве аргумента список инициализации, а не конструктор с одним параметром, создающий контейнер заданного размера. Для вызова этого конструктора пользователь должен будет использовать стандартный синтаксис вызова конструктора.

Основные отличия списка инициализации от тела

Суть в том, что задать значения свойств/полей можно как при помощи списка инициализации, так и в теле самого конструктора. Вся разница в том, что в списке инициализации переменные декларируются и сразу же инициализируются в то время как в теле конструктора созданные переменные лишь задаются значениями/инициализируются. Разницу можно почувствовать, например, при работе с константами классов. Переменную нужно задавать значением сразу при декларации, поэтому определить переменную класса можно лишь в списке инициализации так как в это время она и создаётся. Уже в теле конструктора константу класса задать нельзя так как на этот момент она уже создана и переопределять константы нельзя.

Возвращение списка инициализации

Суть возвращения списков инициализации в том, что бы представлять объект в виде JSON, то есть объект записывается в фигурных скобках. Таким образом можно как инициализировать объект, так и возвращать объект в виде списка инициализации.

Предоставлена возможность писать подобный код:

Последовательность инициализаторов

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

Источник

16.7 – Список инициализаторов std::initializer_list

Рассмотрим фиксированный массив чисел int в C++:

Если мы хотим инициализировать этот массив значениями, мы можем сделать это напрямую с помощью синтаксиса списка инициализаторов:

Это также работает и для динамически размещаемых массивов:

Этот код не компилируется, потому что у класса IntArray нет конструктора, который знает, что делать со списком инициализаторов. В результате нам остается инициализировать элементы массива по отдельности:

Это не так уж и хорошо.

Инициализация класса с помощью std::initializer_list

Этот код дает ожидаемый результат:

Всё работает! Теперь давайте рассмотрим этот код более подробно.

Одно предостережение: списки инициализаторов всегда будут отдавать предпочтение соответствующему конструктору с initializer_list по сравнению с другими потенциально подходящими конструкторами. Таким образом, это определение переменной:

Присваивание объектам класса с использованием std::initializer_list

Почему? Рассмотрим приведенный выше класс (который не имеет перегруженного присваивания со списком инициализаторов или копирующего присваивания) вместе со следующей инструкцией:

Правило

Если вы обеспечиваете создание объекта с использованием списка инициализаторов, неплохо также предоставить присваивание с использованием списка инициализаторов.

Резюме

Небольшой тест

Вопрос 1

Источник

12.6 – Списки инициализаторов членов в конструкторах

В предыдущем уроке для простоты мы инициализировали члены данных нашего класса в конструкторе с помощью оператора присваивания. Например:

Хотя это допустимо в рамках синтаксиса языка C++, но это не демонстрирует хороший стиль (и может быть менее эффективным, чем инициализация).

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

Это создает код, подобный следующему:

Присваивание значений константным или ссылочным переменным-членам в теле конструктора в некоторых случаях невозможно.

Списки инициализаторов членов

Чтобы решить эту проблему, C++ предоставляет метод инициализации переменных-членов класса (вместо присваивания им значений после их создания) через список инициализаторов членов (часто называемый «списком инициализации членов»). Не путайте их с похоже называющимся списком инициализаторов, который мы можем использовать для присваивания значений массивам.

В уроке «1.4 – Присваивание и инициализация переменных» вы узнали, что переменные можно инициализировать тремя способами: через копирующую, прямую и унифицированную инициализацию.

Использование списка инициализации почти идентично выполнению прямой инициализации или унифицированной инициализации.

Это лучше всего изучить на примере. Вернемся к нашему коду, который выполняет присваивания в теле конструктора:

Теперь давайте напишем тот же код, используя список инициализации:

Эта программа печатает:

Список инициализаторов членов класса вставляется после параметров конструктора. Он начинается с двоеточия ( : ), а затем перечисляет через запятые все иницализируемые переменные вместе со значениями этих переменных.

Обратите внимание, что нам больше не нужно выполнять присваивание в теле конструктора, поскольку список инициализаторов заменяет эту функцию. Также обратите внимание, что список инициализаторов не заканчивается точкой с запятой.

Конечно, конструкторы более полезны, когда мы позволяем вызывающему передавать значения инициализации:

Эта программа печатает:

Обратите внимание, что вы можете использовать параметры по умолчанию, чтобы указать значение по умолчанию, если пользователь его не предоставил.

Вот пример класса с константной переменной-членом:

Это работает, потому что нам разрешено инициализировать константные переменные (но не присваивать им значения!).

Правило

Для инициализации переменных-членов вашего класса вместо присваивания используйте списки инициализаторов членов.

Инициализация элементов массива списками инициализаторов членов

Рассмотрим класс с членом-массивом:

До C++11 член-массив с помощью списка инициализации членов класса можно было только обнулить:

Однако, начиная с C++11, вы можете полностью инициализировать член-массив, используя унифицированную инициализацию:

Инициализация переменных-членов, которые являются классами

Список инициализации членов также может использоваться для инициализации членов, которые являются классами.

Эта программа печатает:

Форматирование списков инициализаторов

C++ дает вам большую гибкость в том, как форматировать списки инициализаторов, и вам решать, как вы хотите действовать. Но вот несколько рекомендаций:

Если список инициализаторов умещается в той же строке, что и имя функции, то можно разместить всё в одной строке:

Если список инициализаторов не помещается в той же строке, что и имя функции, он должен быть помещен с отступом на следующей строке.

Если все инициализаторы не помещаются в одну строку (или инициализаторы нетривиальны), вы можете разделить их, по одному на строку:

Порядок в списке инициализаторов

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

Для достижения наилучших результатов следует соблюдать следующие рекомендации:

Резюме

Списки инициализаторов членов позволяют нам инициализировать наши члены, а не присваивать им значения. Это единственный способ инициализировать члены, которым требуются значения при инициализации, например, константные или ссылочные члены, и это может быть более производительным, чем присваивание значений в теле конструктора. Списки инициализаторов членов работают как с базовыми типами, так и с членами, которые сами являются классами.

Небольшой тест

Вопрос 1

Если вам нужно напоминание о том, как использовать целые числа фиксированной ширины, просмотрите урок «4.6 – Целочисленные типы фиксированной ширины и size_t».

Источник

Урок №117. Список инициализации членов класса

Обновл. 13 Сен 2021 |

На этом уроке мы рассмотрим, как инициализировать переменные-члены класса с помощью списка инициализации в языке С++, а также особенности и нюансы, которые при этом могут возникнуть.

Списки инициализации членов класса

На предыдущем уроке мы инициализировали члены нашего класса в конструкторе через оператор присваивания:

Хотя в плане синтаксиса языка C++ вопросов никаких нет — всё корректно, но более эффективно — использовать инициализацию, а не присваивание после объявления.

Как мы уже знаем из предыдущих уроков, некоторые типы данных (например, константы и ссылки) должны быть инициализированы сразу. Рассмотрим следующий пример:

Аналогичен код в не объектно-ориентированном C++:

Для решения этой проблемы в C++ добавили метод инициализации переменных-членов класса через список инициализации членов, вместо присваивания им значений после объявления. Не путайте этот список с аналогичным списком инициализаторов, который используется для инициализации массивов.

Из урока №28 мы уже знаем, что инициализировать переменные можно тремя способами: через копирующую инициализацию, прямую инициализацию или uniform-инициализацию.

Использование списка инициализации почти идентично выполнению прямой инициализации (или uniform-инициализации в C++11).

Чтобы было понятнее, рассмотрим пример. Вот код с присваиванием значений переменным-членам класса в конструкторе:

Теперь давайте перепишем этот код, но уже с использованием списка инициализации:

Результат выполнения программы:

Список инициализации членов находится сразу же после параметров конструктора. Он начинается с двоеточия ( : ), а затем значение для каждой переменной указывается в круглых скобках. Больше не нужно выполнять операции присваивания в теле конструктора. Также обратите внимание, что список инициализации членов не заканчивается точкой с запятой.

Можно также добавить возможность caller-у передавать значения для инициализации:

Результат выполнения программы:

Мы можем использовать параметры по умолчанию для предоставления значений по умолчанию, если пользователь их не предоставил. Например, класс, который имеет константную переменную-член:

Это работает, поскольку нам разрешено инициализировать константные переменные (но не присваивать им значения после объявления!).

Правило: Используйте списки инициализации членов, вместо операций присваивания, для инициализации переменных-членов вашего класса.

uniform-инициализация в C++11

В C++11 вместо прямой инициализации можно использовать uniform-инициализацию:

Настоятельно рекомендуется использовать этот синтаксис (даже если вы не используете константы или ссылки в качестве переменных-членов вашего класса), поскольку списки инициализации членов необходимы при композиции и наследовании (это рассмотрим несколько позже).

Правило: Используйте uniform-инициализацию вместо прямой инициализации в C++11.

Инициализация массивов в классе

Рассмотрим класс с массивом в качестве переменной-члена:

До C++11 мы могли только обнулить массив через список инициализации:

Однако в C++11 вы можете полностью инициализировать массив, используя uniform-инициализацию:

Инициализация переменных-членов, которые являются классами

Список инициализации членов также может использоваться для инициализации членов, которые являются классами:

Результат выполнения программы:

Использование списков инициализации

Если список инициализации помещается на той же строке, что и имя конструктора, то лучше всё разместить в одной строке:

Если список инициализации членов не помещается в строке с именем конструктора, то на следующей строке (используя перенос) инициализаторы должны быть с отступом:

Если все инициализаторы не помещаются на одной строке, то вы можете выделить для каждого инициализатора отдельную строку:

Порядок выполнения в списке инициализации

Удивительно, но переменные в списке инициализации не инициализируются в том порядке, в котором они указаны. Вместо этого они инициализируются в том порядке, в котором объявлены в классе, поэтому следует соблюдать следующие рекомендации:

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

Инициализируйте переменные в списке инициализации в том порядке, в котором они объявлены в классе.

Заключение

Списки инициализации членов позволяют инициализировать члены, а не присваивать им значения. Это единственный способ инициализации констант и ссылок, которые являются переменными-членами вашего класса. Во многих случаях использование списка инициализации может быть более результативным, чем присваивание значений переменным-членам в теле конструктора. Списки инициализации работают как с переменными фундаментальных типов данных, так и с членами, которые сами являются классами.

Напишите класс с именем RGBA, который содержит 4 переменные-члены типа std::uint8_t (подключите заголовочный файл cstdint для доступа к типу std::uint8_t):

Подсказка: Если функция print() работает некорректно, то убедитесь, что вы конвертировали std::uint8_t в int.

Источник

Инициализация в современном C++

что такое список инициализации. Смотреть фото что такое список инициализации. Смотреть картинку что такое список инициализации. Картинка про что такое список инициализации. Фото что такое список инициализации

Общеизвестно, что семантика инициализации — одна из наиболее сложных частей C++. Существует множество видов инициализации, описываемых разным синтаксисом, и все они взаимодействуют сложным и вызывающим вопросы способом. C++11 принес концепцию «универсальной инициализации». К сожалению, она привнесла еще более сложные правила, и в свою очередь, их перекрыли в C++14, C++17 и снова поменяют в C++20.

Под катом — видео и перевод доклада Тимура Домлера (Timur Doumler) с конференции C++ Russia. Тимур вначале подводит исторические итоги эволюции инициализации в С++, дает системный обзор текущего варианта правила инициализации, типичных проблем и сюрпризов, объясняет, как использовать все эти правила эффективно, и, наконец, рассказывает о свежих предложениях в стандарт, которые могут сделать семантику инициализации C++20 немного более удобной. Далее повествование — от его лица.

Table of Contents

что такое список инициализации. Смотреть фото что такое список инициализации. Смотреть картинку что такое список инициализации. Картинка про что такое список инициализации. Фото что такое список инициализации

Гифка, которую вы сейчас видите, отлично доносит основную мысль доклада. Я нашёл её на просторах интернета где-то полгода тому назад, и выложил у себя в твиттере. В комментариях к ней кто-то сказал, что не хватает ещё трёх типов инициализации. Началось обсуждение, в ходе которого мне предложили сделать об этом доклад. Так всё и началось.

Про инициализацию уже рассказывал Николай Йоссутис. В его докладе был слайд, на котором перечислялись 19 различных способов инициализировать int:

Мне кажется, это уникальная ситуация для языка программирования. Инициализация переменной — одно из простейших действий, но в С++ сделать это совсем не просто. Вряд ли в этом языке есть какая-либо другая область, в которой за последние годы было бы столько же отчётов об отклонениях от стандарта, исправлений и изменений. Правила инициализации меняются от стандарта к стандарту, и в интернете есть бесчисленное количество постов о том, как запутана инициализация в C++. Поэтому сделать её систематический обзор — задача нетривиальная.

Я буду излагать материал в хронологическом порядке: вначале мы поговорим о том, что было унаследовано от С, потом о С++98, затем о С++03, С++11, С++14 и С++17. Мы обсудим распространённые ошибки, и я дам свои рекомендации относительно правильной инициализации. Также я расскажу о нововведениях в С++20. В самом конце доклада будет представлена обзорная таблица.

Инициализация по умолчанию (С)

В С++ очень многое унаследовано от С, поэтому с него мы и начнём. В С есть несколько способов инициализации переменных. Их можно вообще не инициализировать, и это называется инициализация по умолчанию. На мой взгляд, это неудачное название. Дело в том, что никакого значения по умолчанию переменной не присваивается, она просто не инициализируется. Если обратиться к неинициализированной переменной в C++ и в С, возникает неопределённое поведение:

То же касается пользовательских типов: если в некотором struct есть неинициализированные поля, то при обращении к ним также возникает неопределённое поведение:

В С++ было добавлено множество новых конструкций: классы, конструкторы, public, private, методы, но ничто из этого не влияет на только что описанное поведение. Если в классе некоторый элемент не инициализирован, то при обращении к нему возникает неопределённое поведение:

Никакого волшебного способа инициализировать по умолчанию элемент класса в С++ нет. Это интересный момент, и в течение первых нескольких лет моей карьеры с С++ я этого не знал. Ни компилятор, ни IDE, которой я тогда пользовался, об этом никак не напоминали. Мои коллеги не обращали внимания на эту особенность при проверке кода. Я почти уверен, что из-за неё в моём коде, написанном в эти годы, есть довольно странные баги. Мне казалось очевидным, что классы должны инициализировать свои переменные.

В C++98 можно инициализировать переменные при помощи member initializer list. Но такое решение проблемы не оптимальное, поскольку это необходимо делать в каждом конструкторе, и об этом легко забыть. Кроме того, инициализация идёт в порядке, в котором переменные объявлены, а не в порядке member initializer list:

В C++11 были добавлены инициализаторы элементов по умолчанию (direct member initializers), которыми пользоваться значительно удобнее. Они позволяют инициализировать все переменные одновременно, и это даёт уверенность, что все элементы инициализированы:

Моя первая рекомендация: когда можете, всегда используйте DMI (direct member initializers). Их можно использовать как со встроенными типами ( float и int ), так и с объектами. Привычка инициализировать элементы заставляет подходить к этому вопросу более осознанно.

Копирующая инициализация (С)

Итак, первый унаследованный от С способ инициализации — инициализация по умолчанию, и ей пользоваться не следует. Второй способ — копирующая инициализация. В этом случае мы указываем переменную и через знак равенства — её значение:

Копирующая инициализация также используется, когда аргумент передаётся в функцию по значению, или когда происходит возврат объекта из функции по значению:

Знак равенства может создать впечатление, что происходит присвоение значения, но это не так. Копирующая инициализация — это не присвоение значения. В этом докладе вообще ничего не будет про присвоение.

Другое важное свойство копирующей инициализации: если типы значений не совпадают, то выполняется последовательность преобразования (conversion sequence). У последовательности преобразования есть определенные правила, например, она не вызывает explicit конструкторов, поскольку они не являются преобразующими конструкторами. Поэтому, если выполнить копирующую инициализацию для объекта, конструктор которого отмечен как explicit, происходит ошибка компиляции:

Более того, если есть другой конструктор, который не является explicit, но при этом хуже подходит по типу, то копирующая инициализация вызовет его, проигнорировав explicit конструктор:

Агрегатная инициализация (С)

Третий тип инициализации, о котором я хотел бы рассказать — агрегатная инициализация. Она выполняется, когда массив инициализируется рядом значений в фигурных скобках:

Если при этом не указать размер массива, то он выводится из количества значений, заключённых в скобки:

Эта же инициализация используется для агрегатных (aggregate) классов, то есть таких классов, которые являются просто набором публичных элементов (в определении агрегатных классов есть ещё несколько правил, но сейчас мы не будем на них останавливаться):

Этот синтаксис работал ещё в С и С++98, причём, начиная с С++11, в нём можно пропускать знак равенства:

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

А если для этих объектов есть другой конструктор, не-explicit, то вызывается он, даже если он хуже подходит по типу:

Рассмотрим ещё одно свойство агрегатной инициализации. Вопрос: какое значение возвращает эта программа?

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

Статическая инициализация (С)

Наконец, от С также унаследована статическая инициализация: статические переменные всегда инициализируются. Это может быть сделано несколькими способами. Статическую переменную можно инициализировать выражением-константой. В этом случае инициализация происходит во время компиляции. Если же переменной не присвоить никакого значения, то она инициализируется значением нуль:

Эта программа возвращает 3, несмотря на то, что j не инициализировано. Если же переменная инициализируется не константой, а объектом, могут возникнуть проблемы.

Вот пример из реальной библиотеки, над которой я работал:

Итак, от языка C унаследованы четыре типа инициализации: инициализация по умолчанию, копирующая, агрегатная и статическая инициализации.

Прямая инициализация (С++98)

Перейдём теперь к С++98. Пожалуй, наиболее важная возможность, отличающая С++ от С — это конструкторы. Вот пример вызова конструктора:

Кроме того, при прямой инициализации не выполняется последовательность преобразования. Вместо этого происходит вызов конструктора при помощи разрешения перегрузки (overload resolution). У прямой инициализации тот же синтаксис, что и у вызова функции, и используется та же логика, что и в других функциях С++.

Поэтому в ситуации с explicit конструктором прямая инициализация работает нормально, хотя копирующая инициализация выдаёт ошибку:

В ситуации же с двумя конструкторами, один из которых explicit, а второй хуже подходит по типу, при прямой инициализации вызывается первый, а при копирующей — второй. В такой ситуации изменение синтаксиса приведёт к вызову другого конструктора — об этом часто забывают:

Прямая инициализация применяется всегда, когда используются круглые скобки, в том числе когда используется нотация вызова конструктора для инициализации временного объекта, а также в выражениях new с инициализатором в скобках и в выражениях cast :

Этот синтаксис существует столько, сколько существует сам С++, и у него есть важный недостаток, который упомянул Николай в программном докладе: the most vexing parse. Это значит, что всё, что компилятор может прочитать как объявление (declaration), он читает именно как объявление.

Инициализация значением (C++03)

Перейдём к следующей версии — С++03. Принято считать, что существенных изменений в этой версии не произошло, но это не так. В С++03 появилась инициализация значением (value initialization), при которой пишутся пустые круглые скобки:

В С++98 здесь возникает неопределенное поведение, потому что происходит инициализация по умолчанию, а начиная с С++03 эта программа возвращает нуль.

Правило такое: если существует определённый пользователем конструктор по умолчанию, инициализация значением вызывает этот конструктор, в противном случае возвращается нуль.

Рассмотрим подробнее ситуацию с пользовательским конструктором:

Стоит заметить, что «пользовательский» не значит «определённый пользователем». Это значит, что пользователь должен предоставить тело конструктора, т. е. фигурные скобки. Если же в примере выше заменить тело конструктора на = default (эта возможность была добавлена в С++11), смысл программы изменяется. Теперь мы имеем конструктор, определённый пользователем (user-defined), но не предоставленный пользователем (user-provided), поэтому программа возвращает нуль:

Теперь попробуем вынести Widget() = default за рамки класса. Смысл программы снова изменился: Widget() = default считается предоставленным пользователем конструктором, если он находится вне класса. Программа снова возвращает неопределённое поведение.

Универсальная инициализация (C++11)

В версии С++11 было много очень важных изменений. В частности, была введена универсальная (uniform) инициализация, которую я предпочитаю называть «unicorn initialization» («инициализация-единорог»), потому что она просто волшебная. Давайте разберёмся, зачем она появилась.

Все эти проблемы создатели языка попытались решить, введя синтаксис с фигурными скобками но без знака равенства. Предполагалось, что это будет единый синтаксис для всех типов, в котором используются фигурные скобки и не возникает проблемы vexing parse. В большинстве случаев этот синтаксис выполняет свою задачу.

Эта новая инициализация называется инициализация списком, и она бывает двух типов: прямая и копирования. В первом случае используются просто фигурные скобки, во втором — фигурные скобки со знаком равенства:

Мне кажется, что со стороны комитета С++ std::initializer_list был не самым удачным решением. От него больше вреда, чем пользы.

Далее, std::initializer_list является объектом. Используя его, мы, фактически, создаём и передаём объекты. Как правило, компилятор может это оптимизировать, но с точки зрения семантики мы всё равно имеем дело с лишними объектами.

Если вызвать vector с двумя аргументами int и использовать прямую инициализацию, то выполняется вызов конструктора, который первым аргументом принимает размер вектора, а вторым — значение элемента. На выходе получается вектор из трёх нулей. Если же вместо круглых скобок написать фигурные, то используется initializer_list и на выходе получается вектор из двух элементов, 3 и 0.

Есть примеры ещё более странного поведения этого синтаксиса:

Ещё больше трудностей возникает при использовании шаблонов. Как вы думаете, что возвращает эта программа? Какой здесь размер вектора?

Теперь давайте разберёмся, что именно делает инициализация списком.

Для агрегатных типов при такой инициализации выполняется агрегатная
инициализация.
Для встроенных типов — прямая инициализация ( ) или
копирующая инициализация ( = );
А для классов выполняется такая последовательность:

Для второго шага есть пара исключений.

Но бывают случаи, когда от этой конструкции только вред. Давайте рассмотрим такой случай:

Идём дальше. Передача и возврат braced-init-list также является инициализацией копированием списка. Это очень полезное свойство:

Если происходит возврат по значению, то используется инициализация копированием, поэтому при возврате braced-init-list используется инициализация копированием списка. А если передать braced-init-list функции, это также приведёт к инициализации копированием списка.

Конечно, это приводит к некоторым затруднениям в случае со вложенными скобками. На StackOverflow недавно был замечательный пост, в котором рассматривался один и тот же вызов функции с разными уровнями вложенности. Выяснилось, что результаты на всех уровнях разные. Я не буду вдаваться в подробности, потому что там всё очень сложно, но сам этот факт показателен:

Улучшения в С++14

Итак, мы прошли все версии до C++11 включительно. Мы обсудили все инициализации прошлых версий, плюс инициализацию списком, которая часто работает по совсем не очевидным правилам. Поговорим теперь о C++14. В нём были исправлены некоторые проблемы, доставшиеся от прошлых версий.

Например, в С++11 у агрегатных классов не могло быть direct member initializers, что вызывало совершенно ненужные затруднения. Выше я уже говорил о том, что direct member initializers очень полезны. Начиная с С++14, у агрегатных классов могут быть direct member initializers:

Наконец, в C++14 была решена проблема со статической инициализацией, но она была значительно менее важной, чем те, о которых я сейчас рассказал, и останавливаться на ней мы не будем. Если есть желание, об этом можно почитать самостоятельно.

Несмотря на все эти фиксы, в С++14 осталось много проблем с инициализацией списком:

Сам std::initializer_list не работает с move-only типами.

Синтаксис практичеcки бесполезен для шаблонов, поэтому emplace или make_unique нельзя использовать для агрегатных типов.

Есть некоторые неочевидные правила, о которых мы уже говорили:

Наконец, я еще не рассказал, что инициализация списка совсем не работает с макросами.

Пример про макросы: assert(Widget(2,3)) выполняется, а assert(Widget<2,3>) ломает препроцессор. Дело в том, что у макросов есть специальное правило, которое правильно читает запятую внутри круглых скобок, но оно не было обновлено для фигурных скобок. Поэтому запятая в этом примере рассматривается как конец первого аргумента макроса, хотя скобки ещё не закрыты. Это приводит к сбою.

Как правильно инициализировать в C++

Я могу предложить несколько советов относительно того, как правильно инициализировать значения в С++.

Для простых типов вроде int используйте инициализацию копированием, т. е. знак равенства и значение — так делается в большинстве языков программирования, к этому все давно привыкли и это наиболее простой вариант.

Кроме того, фигурными скобками удобно пользоваться для передачи и возвращения врéменных объектов. При помощи двух пустых фигурных скобок можно быстро сделать инициализацию значения временного объекта.

Можно даже пропустить имя типа и использовать braced-init-list — это работает только с фигурными скобками.

= value для простых типов

и <> для передачи и возврата врéменных объектов

(args) для вызова конструкторов

Смысл тот же, но так вы никогда не забудете инициализировать переменную. Больше того, если следовать этой рекомендации и писать тип в правой части выражения, то не возникает проблемы vexing parse:

Изначально это правило формулировалось как «почти всегда auto» («almost always auto», AAA), поскольку в С++11 и С++14 при таком написании код не всегда компилировался, как, например, в случае с таким std::atomic :

Дело в том, что atomic нельзя перемещать и копировать. Несмотря на то, что в нашем синтаксисе никакого копирования и перемещения не происходит, всё равно было требование, чтобы использовался соответствующий конструктор, хоть вызова к нему и не происходило. В С++17 эта проблема была решена, было добавлено новое свойство, которое называется гарантированный пропуск копирования (guaranteed copy elision):

В С++17 также была добавлена CTAD (class template argument deduction). Оказалось, что у этого свойства есть довольно странные и не всегда очевидные следствия для инициализации. Эту тему уже затрагивал Николай в программном докладе. Кроме того, в прошлом году я выступал с докладом на CppCon, целиком посвящённым CTAD, там обо всём этом рассказано значительно подробнее. По большому счёту, в С++17 ситуация та же, что и в С++11 и С++14, за исключением того, что были исправлены некоторые самые неудобные неисправности. Инициализация списком сейчас работает лучше, чем в прошлых версиях, но, на мой взгляд, в ней ещё многое можно улучшить.

Назначенная инициализация (С++20)

Теперь давайте поговорим о С++20, то есть о грядущих изменениях. И да, вы угадали, в этом новом стандарте появится ещё один способ инициализации объектов: назначенная инициализация (designated initialization):

Важное преимущество такого подхода в том, что здесь, как и при агрегатной инициализации, не может быть неинициализированных переменных. Работает такая инициализация только с агрегатными типами, то есть фактически это другой синтаксис для агрегатной инициализации.

Сделано это было для совместимости с С, и работает так же, как в С99, с некоторыми исключениями:

в С не нужно соблюдать порядок элементов, то есть в нашем примере можно сначала инициализировать с, а потом а. В С++ так делать нельзя, поскольку вещи конструируются в порядке, в котором они объявлены. :

К сожалению, это ограничивает применимость этой конструкции.

в С++ нельзя одновременно использовать назначенную и обычную инициализацию, но лично мне сложно придумать ситуацию, в которой это следовало бы делать:

в С++ этот вид инициализации нельзя использовать с массивами. Но, опять-таки, я не думаю, что это вообще следует делать.

Исправления в C++20

Помимо нового вида инициализации в С++20 будут исправлены некоторые вещи из предыдущих версий, и некоторые из этих изменений были предложены мной. Обсудим одно из них (wg21.link/p1008).

Когда в С++17 удаляется конструктор по умолчанию, это скорее всего значит, что автор кода хочет запретить создание экземпляров объекта. В агрегатных типах с удалённым конструктором по умолчанию инициализация по умолчанию выдаёт ошибку, но агрегатная инициализация работает, и это позволяет обойти удаление конструктора, сделанное автором класса:

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

Об этом просто забыли, когда в С++11 создавали braced-init-list. В С++ это будет исправлено. Вряд ли много людей сталкивалось с этой проблемой, но исправить её полезно для согласованности языка.

Прямая инициализация агрегатных типов (C++20)

Наконец, в С++20 будет добавлен ещё один способ инициализации. Я уже говорил о неудобствах инициализации списком, из них в особенности неприятна невозможность использовать её с шаблонами и с макросами. В С++20 это исправят: можно будет использовать прямую инициализацию для агрегатных типов (wg21.link/p0960).

Кроме того, эта новая возможность будет работать с массивами:

На мой взгляд, это очень важно: назовём это uniform инициализацией 2.0. Вновь будет достигнута некоторая однородность. Если агрегатную инициализацию можно будет выполнять и с фигурными, и с круглыми скобками, то, в сущности, круглые и фигурные скобки будут делать почти одно и то же. Исключение — конструктор initializer_list : если необходимо его вызвать, надо использовать фигурные скобки, если нет — круглые. Это позволяет однозначно указать, что именно нам необходимо. Кроме того, фигурные скобки по-прежнему не будут выполнять сужающие преобразования, а круглые — будут. Это делается для однородности с вызовами конструктора.

Я подвёл итог всему, что мы сегодня обсуждали, в таблице. Строки в этой таблице — различные типы, а столбцы — синтаксисы инициализации. На этом у меня всё, спасибо большое за внимание.

что такое список инициализации. Смотреть фото что такое список инициализации. Смотреть картинку что такое список инициализации. Картинка про что такое список инициализации. Фото что такое список инициализации

Уже совсем скоро, в конце октября, Тимур приедет на C++ Russia 2019 Piter и выступит с докладом «Type punning in modern C++». Тимур расскажет про новые техники, представленные в С++20, и покажет, как их безопасно использовать, а также разберёт «дыры» в С++ и объяснит, как их можно пофиксить.

Источник

Добавить комментарий

Ваш адрес email не будет опубликован. Обязательные поля помечены *