Что такое инициализация в программировании
Перейти к содержимому

Что такое инициализация в программировании

  • автор:

Что такое инициализация в программировании

14. Инициализация, присваивание и

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

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

14.1. Инициализация класса

int ival; char *ptr;

Рассмотрим следующее определение класса:

Чтобы безопасно пользоваться объектом класса, необходимо правильно инициализировать его члены. Однако смысл этого действия для разных классов различен. Например, может ли ival содержать отрицательное значение или нуль? Каковы правильные начальные значения обоих членов класса? М1 не ответим на эти вопросы, не понимая абстракции, представляемой классом. Если с его помощью описываются служащие компании, то ptr, вероятно, указывает на фамилию служащего, а ival — его уникальный номер. Тогда отрицательное или нулевое значения ошибочны. Если же класс представляет текущую температуру в городе, то допустимы любые значения ival. Возможно также, что класс Data представляет строку со счетчиком ссылок: в таком случае ival содержит текущее число ссылок на строку по адресу ptr. При такой абстракции ival инициализируется значением 1; как только значение становится равным 0, объект класса уничтожается.

Мнемонические имена класса и обоих его членов сделали бы, конечно, его назначение более понятным для читателя программы, но не дали бы никакой дополнительной

Data dat01( Venus and the Graces , 107925 ); Data dat02( about );

Data dat03( 107925 );

Бывают ситуации (как в случае с dat04), когда нам нужен объект класса, но его начальные значения мы еще не знаем. Возможно, они станут известны позже. Однако начальное значение задать необходимо, хотя бы такое, которое показывает, что разумное начальное значение еще не присвоено. Другими словами, инициализация объекта иногда сводится к тому, чтобы показать, что он еще не инициализирован. Большинство классов предоставляют специальный конструктор по умолчанию, для которого не требуется задавать начальных значений. Как правило, он инициализирует объект таким образом, чтобы позже можно было понять, что реальной инициализации еще не проводилось.

Обязан ли наш класс Data иметь конструктор? Нет, поскольку все его члены открыты. Унаследованный из языка C механизм поддерживает явную инициализацию,

local1.ival = 0; local1.ptr = 0 Data local1 = < 0, 0 >;

local3.ptr = Anna Livia Plurabelle Data.local2 — < 1024, Anna Livia Plurabelle >;

аналогичную используемой при инициализации массивов:

Значения присваиваются нозиционно, на основе порядка, в котором объявляются данные-члены. Следующий пример приводит к ошибке компиляции, так как ival объявлен перед

ошибка: ival = AAnna Livia Plurabelle ;

Явная инициализация имеет два основных недостатка. Во-нервых, она может быть применена лишь для объектов классов, все члены которых открыты (т.е. эта инициализация не поддерживает инкапсуляции данных и абстрактных типов — их не было в языке C, откуда она заимствована). А во-вторых, такая форма требует

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

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

Так нужно ли применять явную инициализацию вместо конструкторов? Да. Для некоторых приложений более эффективно использовать список для инициализации больших структур постоянными значениями. К примеру, мы можем таким образом построить палитру цветов или включить в текст программы фиксированные координаты вершин и значения в узлах сложной геометрической модели. В подобных случаях инициализация выполняется во время загрузки, что сокращает затраты времени на запуск конструктора, даже если рн определен как встроенный. Это особенно удобно при работе с глобальными объектами1.

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

Примечание [O.A.4]: Нумера ция сносок сбита.

14.2. Конструктор класса

Среди других функций-членов конструктор выделяется тем, что его имя совпадает с

конструктор по умолчанию . Account();

unsigned int acct nmbr; double balance;

именем класса. Для объявления конструктора по умолчанию мы пишем2:

Единственное синтаксическое ограничение, налагаемое на конструктор, состоит в том, что он не должен иметь тип возвращаемого значения, даже void. Поэтому следующие

ошибки: у конструктора не

может быть типа возвращаемого значения

Account* Account::Account( const char *pc )

1 Более подробное обсуждение этой темы с примерами оценками производительности см. в [LIPPMAN96a].

2 В реальной программе мы объявили бы член name как имеющий тип string. Здесь он объявлен как C-строка, чтобы отложить рассмотрение вопроса об инициализации членов класса до раздела 14.4.

Зачем нужна инициализация в программировании? Какие есть виды инициализации?

Что Вы подразумеваете под термином инициализация? Переменные, классы, массивы? Если Вас интересует инициализация переменных — это можно так сказать процесс присваивания каких-либо данных или процесс внесения информация с клавиатуры. Если же Вас интересуют классы .. то инициализация класса — это точка входа в проект . Вот в Java программирование без объявления (инициализации) класса Вам никак не обойтись . Так как класс в Java — это есть точка вход в проект .

-Egor-Мыслитель (6178) 10 лет назад

точка входа в Java это статический метод, никакой инициализации класса там нету.

Инициализация в большенстве языков програмирования — это процесс подготовки (выделение) памяти под программные нужды . Бывает статическая и динамическая. Статическая подготавливает память сразу же на запуске программы, динамическая по необходимосте (по какому то сигналу от программы)
Как из простых примеров : обьявления массива и каких либо констант . в разных языках синтаксис разный в паскале это область var, в java это либо конструктор класса либо статическая область static<>

Похожие вопросы

Объявление? Определение? Инициализация?

В чем разница между объявлением переменной, её определением и инициализацией

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

Такими близкими по смыслу и довольно часто путающимися являются понятия «объявление» переменной, её «определение» и её «инициализация».

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

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

Объявление переменной или константы

Итак, выражение называется объявлением переменной или декларированием. На английском это выглядит так — Declaring Constants and Variables:

var a = 10

В приведенном примере задана переменная (var), задано имя переменной (а) и задано значение переменной (10).

Определение переменной или константы

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

var b: Int?

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

Заодно и еще один термин — аннотация типа. Это вот как раз результат действия по определению, а именно слово Int?.

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

var b: (Int, Int, Int)

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

Инициализация переменной или константы

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

var a = 10

Инициализация означает, что переменная запущена в работу, ей присвоено начальное значение, она инициализирована. Без присвоения начального значения переменная просто объявлена, а с начальным значением она еще и инициализирована.

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

var b: Int

Три действия — объявление, определение, инициализация

Таким образом, мы имеем здесь три действия — объявление, определение и инициализацию. Их можно записать в одном выражении:

var a: Int = 10

Но, как мы помним, Swift рекомендует экономить время, если есть значение, он может самостоятельно определить тип данных.

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

Ковбойская история

Разницу между этими тремя действиями можно проиллюстрировать с помощью шуточного примера, найденного на просторах интернета и немного подредактированного))

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

Бармен спрашивает — чем будешь платить: деньгами, отдашь пистолет или отработаешь мытьем посуды? — Я заплачу, — отвечает ковбой, — у меня есть деньги. — Это определение типа данных.

Тогда плати 5 баксов и получай выпивку — Ковбой платит и веселье начинается — Это инициализация, присвоение начального значения, запуск процесса в работу.

Заметка

Обратите внимание, что мы используем выражение «тип данных» и по отношению к значению, и по отношению к переменным и константам. Дело в том, что в программировании по большому счета можно практически все называть типом данных. Это и объекты, и классы, и наиболее нам известные Int, String, Double, Bool и т.д.
Но, чтобы как-то все же отличать типы данных значений от констант и переменных будем называть последние некими сущностями, элементами языка программирования. Тогда у нас будут типы данных для значений и типы данных для определенных элементов программирования (к которым относятся в том числе константы и переменные).

Автор: Максим Савостьянов

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

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

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

Table of Contents

  • Инициализация по умолчанию (С)
  • Копирующая инициализация (С)
  • Агрегатная инициализация (С)
  • Статическая инициализация (С)
  • Прямая инициализация (С++98)
  • Инициализация значением (C++03)
  • Универсальная инициализация (C++11)
  • Улучшения в С++14
  • Как правильно инициализировать в C++
  • Назначенная инициализация (С++20)
  • Исправления в C++20
  • Прямая инициализация агрегатных типов (C++20)

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

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

int i1; //undefined value int i2 = 42; //note: inits with 42 int i3(42); //inits with 42 int i4 = int(); //inits with 42 int i5; //inits with 42 int i6 = ; //inits with 42 int i7<>; //inits with 0 int i8 = <>; //inits with 0 auto i9 = 42; //inits with 42 auto i10; //C++11: std::initializer_list, C++14: int auto i11 = ; //inits std::initializer_list with 42 auto i12 = int; //inits int with 42 int i13(); //declares a function int i14(7, 9); //compile-time error int i15 = (7, 9); //OK, inits int with 9 (comma operator) int i16 = int(7, 9); //compile-time error int i17(7, 9); //compile-time error auto i18 = (7, 9); //OK, inits int with 9 (comma operator) auto i19 = int(7, 9); //compile-time error

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

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

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

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

int main() < int i; return i; // undefined behaviour >

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

struct Widget < int i; int j; >; int main() < Widget widget; return widget.i; // неопределенное поведение >

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

class Widget < public: Widget() <>int get_i() const noexcept < return i; >int get_j() const noexcept < return j; >private: int i; int j; >; int main() < Widget widget; return widget.get_i(); // Undefined behaviour! >

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

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

// C++98: member initialiser list class Widget < public: Widget() : i(0), j(0) <>// member initialiser list int get_i() const noexcept < return i; >int get_j() const noexcept < return j; >private: int i; int j; >; int main()

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

// C++11: default member initialisers class Widget < public: Widget() <>int get_i() const noexcept < return i; >int get_j() const noexcept < return j; >private: int i = 0; // default member initialisers int j = 0; >; int main()

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

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

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

// copy initialization int main()

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

// copy initialization int square(int i)

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

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

struct Widget < explicit Widget(int) <>>; Widget w1 = 1; // ERROR

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

struct Widget < explicit Widget(int) <>Widget(double) <> >; Widget w1 = 1; // вызывает Widget(double)

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

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

int i[4] = ;

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

int j[] = ; // array size deduction

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

struct Widget < int i; float j; >; Widget widget = ;

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

Widget widget;

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

struct Widget < explicit Widget(int) <>>; struct Thingy < Widget w1, w2; >; int main() < Thingy thingy = ; // ERROR Thingy thingy ; // ERROR >

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

struct Widget < explicit Widget(int) <>Widget(double) <> >; struct Thingy < Widget w1, w2; >; int main() < Thingy thingy = ; // вызывает Widget(double) Thingy thingy ; // вызывает Widget(double) >

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

struct Widget < int i; int j; >; int main() < Widget widget = ; return widget.j; >

Скрытый текст

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

// все элементы инициализируются нулями int[100] = <>;

Другое важное свойство агрегатной инициализации — пропуск скобок (brace elision). Как вы думаете, какое значение возвращает эта программа? В ней есть Widget , который является агрегатом двух значений int , и Thingy , агрегат Widget и int . Что мы получим, если передадим ей два инициализирующих значения: ?

struct Widget < int i; int j; >; struct Thingy < Widget w; int k; >; int main() < Thingy t = ; return t.k; // что мы получим? >

Скрытый текст

Ответ: нуль. Здесь мы имеем дело с подагрегатом (subaggregate), то есть с вложенным агрегатным классом. Такие классы можно инициализировать, используя вложенные скобки, но одну из этих пар скобок можно пропустить. В этом случае выполняется рекурсивный обход субагрегата, и оказывается эквивалентно <, 0> . Надо признать, это свойство не вполне очевидное.

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

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

static int i = 3; // инициализация константой statit int j; // инициализация нулем int main()

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

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

static Colour red = ;

В ней был класс Colour, и основные цвета (red, green, blue) были определены как статические объекты. Это допустимое действие, но как только появляется другой статический объект, в инициализаторе которого используется red , появляется неопределённость, поскольку нет жёсткого порядка, в котором инициализируются переменные. Ваше приложение может обратиться к неинициализированной переменной, и тогда оно упадёт. К счастью, в С++11 стало возможным использовать конструктор constexpr , и тогда мы имеем дело с инициализацией константой. В этом случае никаких проблем с порядком инициализации уже не возникает.

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

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

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

Widget widget(1, 2); int(3);

При помощи этого же синтаксиса можно инициализировать встроенные типы вроде int и float . Этот синтаксис называется прямой инициализацией. Она выполняется всегда, когда у нас есть аргумент в круглых скобках.

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

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

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

struct Widget < explicit Widget(int) <>>; Widget w1 = 1; // ошибка Widget w2(1); // а так можно

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

struct Widget < explicit Widget(int) <>Widget(double) <> >; Widget w1 = 1; // вызывает Widget(double) Widget w2(1); // вызывает Widget(int)

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

useWidget(Widget(1, 2)); // вызов конструктора auto* widget_ptr = new Widget(2, 3); // new-expression with (args) static_cast(thingy); // cast

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

Рассмотрим пример, в котором есть класс Widget и класс Thingy , и конструктор Thingy , который получает Widget :

struct Widget <>; struct Thingy < Thingy(Widget) <>>; int main ()

На первый взгляд кажется, что при инициализации Thingy ему передаётся созданный по умолчанию Widget , но на самом деле здесь происходит объявление функции. Этот код объявляет функцию, которая получает на вход другую функцию, которая ничего не получает на вход и возвращает Widget , а первая функция возвращает Thingy . Код скомпилируется без ошибок, но вряд ли мы добивались именно такого поведения.

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

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

int main() < return int(); // UB в C++98, 0 начиная с C++03 >

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

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

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

struct Widget < int i; >; Widget get_widget() < return Widget(); // value initialization >int main()

В этой программе функция инициализирует значение для нового Widget и возвращает его. Мы вызываем эту функцию и обращаемся к элементу i объекта Widget . Начиная с C++03 возвращаемое значение здесь нуль, поскольку нет пользовательского конструктора по умолчанию. А если такой конструктор существует, но не инициализирует i , то мы получим неопределённое поведение:

struct Widget < Widget() <>// пользовательский конструктор int i; >; Widget get_widget() < return Widget(); // value initialization >int main() < return get_widget().i; // значение не инициализировано, происходит UB >

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

struct Widget < Widget() = default; // user-defined, но не user-provided int i; >; Widget get_widget() < return Widget(); // value initialization >int main() < return get_widget().i; // возвращает 0 >

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

struct Widget < Widget(); int i; >; Widget::Widget() = default; // вне класса, считается user-provided Widget get_widget() < return Widget(); // value initialization >int main() < return get_widget().i; // снова значение не инициализировано, UB >

Тут есть определённая логика: конструктор, определённый вне класса, может быть внутри другой единицы трансляции. Компилятор может не увидеть этот конструктор, поскольку он может быть в другом файле .cpp . Поэтому делать какие-либо выводы о таком конструкторе компилятор не может, и он не может отличить конструктор с телом от конструктора с = default .

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

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

Как вы уже заметили, в С++ очень много различных синтаксисов инициализации с разным поведением. Множество неудобств вызывала проблема vexing parse с круглыми скобками. Ещё разработчикам не нравилось, что агрегатную инициализацию можно было использовать только с массивами, но не с контейнерами вроде std::vector . Вместо неё приходилось выполнять .reserve и .push_back , или пользоваться всякими жуткими библиотеками:

// вот так было нельзя, а хотелось: std::vector vec = ; // приходилось писать так: std::vector vec; vec.reserve(5); vec.push_back(0); vec.push_back(1); vec.push_back(2); vec.push_back(3); vec.push_back(4);

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

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

// direct-list-initialization Widget widget; // copy-list-initialization Widget widget = ;

Используемый для иницализации список называется braced-init-list. Важно, что этот список не является объектом, у него нет типа. Переход на С++11 с более ранних версий не создаёт никаких проблем с агрегатными типами, так что это изменение не является критическим. Но теперь у списка в фигурных скобках появились новые возможности. Хоть у него и нет типа, он может быть скрыто преобразован в std::initializer_list , это такой специальный новый тип. И если есть конструктор, принимающий на вход std::initializer_list , то вызывается именно этот конструктор:

template class vector < //. vector(std::initializer_listinit); // конструктор с initializer_list >; std::vector vec; // вызывает этот^ конструктор

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

Начнём с того, что std::initializer_list — это вектор фиксированного размера с элементами const . То есть это тип, у него есть функции begin и end , которые возвращают итераторы, есть собственный тип итератора, и чтобы его использовать, нужно включать специальный заголовок. Поскольку элементы std::initializer_list являются const , его нельзя перемещать, поэтому, если T в коде выше является типом move-only, код не будет выполняться.

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

Несколько месяцев назад в твиттере был опрос: если бы можно было отправиться в прошлое и убрать что-либо из C++, что бы вы убрали? Больше всего голосов получил именно initializer_list .

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

Давайе разберёмся, как работает новый синтаксис. Он вызывает конструкторы, которые принимают на вход initializer_list , и эти вызовы создают много проблем по сравнению с прямой инициализацией в старом синтаксисе. Часто приводят следующий пример:

std::vector v(3, 0); // вектор содержит 0, 0, 0 std::vector v; // вектор содержит 3, 0

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

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

std::string s(48, 'a'); // "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa" std::string s; // "0a"

В первой строке создаётся строка из 48 символов «а», а во второй строка «0а». Это происходит потому, что конструктор string принимает на вход initializer_list из символов. 48 является целочисленным значением, поэтому оно преобразуется в символ. В ASCII число 48 — код символа «0». Это очень странно, потому что есть конструктор, принимающий именно такие аргументы, int и char . Но вместо вызова этого конструктора происходит совершенно неочевидное преобразование. В итоге получается код, который чаще всего ведёт себя не так, как мы ожидаем.

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

template auto test() < return std::vector; > int main () < return test().size(); >

Скрытый текст

Мы получим вектор с тремя строками, то есть ответ — 3. Но если string заменить на int , ответ будет 1, потому что для std::vector будет использован initializer_list . В зависимости от шаблонного параметра вызывается либо конструктор initializer_list , либо другой конструктор. А если вместо string или int использовать float , я и вовсе не знаю, что выйдет. Предсказать поведение такого кода очень сложно, и это создаёт множество неудобств. Например, мы не можем написать emplace функцию, которая работала бы для агрегатных типов с синтаксисом фигурных скобок. В общем, агрегатная инициализиация и синтаксис <> не работают с шаблонами.

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

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

  1. Вначале «жадно» выполняется вызов конструктора, который принимает std::initializer_list .
    Если для этого вызова необходимо сделать неочевидные преобразования — они выполняются.
  2. Если подходящего конструктора нет, выполняется обычный
    вызов конструктора () при помощи разрешения перегрузки.

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

Исключение 2: пустые фигурные скобки, <> .
Пусть у нас будет тип с конструктором по умолчанию и конструктором, который принимает initializer_list .
Что происходит при вызове Widget widget<>\ ?

template Typename struct Widget < Widget(); Widget(std::initializer_list); >; int main() < Widgetwidget<>; // какой конструктор будет вызван? >

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

Разберём подробнее инициализацию значением при использовании <> . Здесь, опять-таки, нужно помнить, что при Widget() = default и Widget() <> наблюдается разное поведение — об этом мы уже говорили.

struct Widget < Widget() = default; int i; >; int main() < Widget widget<>; // инициализация значением (нулем), не происходит vexing parse return widget.i; // возвращает 0 >
struct Widget < Widget() <>; // user-provided конструктор int i; >; int main() < Widget widget<>; // инициализация значением, вызывается дефолтный конструктор return widget.i; // не инициализирована, возникает UB >

У инициализации списком есть полезное свойство: не допускаются преобразования, сужающие диапазон значений (narrowing conversions). Если для инициализации int использовать double , это является сужающим преобразованием, и такой код не компилируется:

int main() < int i; // ошибка! >

То же самое происходит, если агрегатный объект инициализировать списком элементов double . Это нововведение C++11, и оно вызывает больше всего ошибок при обновлении кода, написанного на более старых версиях языка. Это создаёт много работы при поддержке больших объёмов унаследованного кода:

struct Widget < int i; int j; >; int main() < Widget widget = ; // ошибка в С++11 в отличие от C++98/03 >

Далее, при инициализации списком можно использовать вложенные фигурные скобки, но, в отличие от агрегатной инициализации, с ними не работает пропуск скобок (brace elision). С одной стороны, использовать вложенные фигурные скобки бывает очень полезно, они вносят ясность. Например, у нас есть map . Тогда внешние фигурные скобки инициализируют этот map , а внутренние фигурные скобки — его элементы:

std::map my_map , >;

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

std::vector v1 ; // OK std::vector v2 <>; // ??

Напомню, это не агрегатная инициализация, это инициализация списком с initializer_list . В первой строке мы используем initializer_list из двух строк, поэтому в результате, очевидно, получается вектор из двух строк. А если заключить эти скобки в ещё одну пару фигурных скобок, получается неопределённое поведение. Попробуем разобраться, почему.

Внешний initializer_list имеет только один элемент — внутренний initializer_list , так что мы получим вектор с одной строкой. Эта строка инициализируется внутренним списком, в котором два const char* . Оказывается, у string есть конструктор, принимающий на вход итераторы char для начала и конца. Так что эти две строки преобразуются в итераторы. Далее выполняется чтение с начала, оно доходит до неинициализированной памяти, и программа падает.

  • читайте списки с фигурными скобками снаружи вовнутрь;
  • без агрегатного типа пропуск скобок не работает.

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

Widget f1() < return ; // copy-list инициализация возвращаемого значения > void f2(Widget); f2(); // copy-list инициализация аргумента

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

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

#include struct A < A() <>A(const A&) <> >; struct B < B(const A&) <>>; void f(const A&) < std::cout void f(const B&) < std::cout int main() < A a; f( ); // A f( <> ); // ambiguous f( > ); // B f(<>>); // no matching function >

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

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

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

struct Widget < int i = 0; int j = 0; >; Widget widget; // работает начиная с C++14

Второе улучшение Николай уже упоминал в программном докладе, оно связано с auto . Если в С++11 после auto следовал braced-init-list, это всегда приводило к выведению типа std::initializer_list :

int i = 3; // int int i(3); // int int i; // int int i = ; // int auto i = 3; // int auto i(3); // int auto i; // В С++11 — std::initializer_list auto i = ; // В С++11 — std::initializer_list

Такое поведение нежелательно: когда пишут auto i , чаще всего имеют ввиду int , а не std::initializer_list . В С++14 это поведение изменили, и auto i теперь читается как int . Если же в фигурных скобках в этом примере несколько значений, то такой код не компилируется. Впрочем, auto i = всегда читается как std::initializer_list . Как видим, здесь всё равно остаётся непоследовательность: при прямой инициализации списка получается int , а при копирующей инициализации — initializer_list .

auto i = 3; // int auto i(3); // int auto i; // в С++14 — int, но работает только для списка из одного элемента auto i = ; // так и осталось std::initializer_list

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

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

  • Не сразу понятно, вызывается ли конструктор, принимающий std::initializer_list .
  • Сам std::initializer_list не работает с move-only типами.
  • Синтаксис практичеcки бесполезен для шаблонов, поэтому emplace или make_unique нельзя использовать для агрегатных типов.
  • Есть некоторые неочевидные правила, о которых мы уже говорили:
    • пустые фигурные скобки ведут себя иначе, чем не-пустые;
    • вложенные фигурные скобки ведут себя неочевидным образом;
    • auto работает не всегда очевидным образом.

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

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

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

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

    Фигурные скобки хороши в других ситуациях: для агрегатной инициализации, для вызова конструкторов, принимающих std::initializer_list , и для direct member initializers. В последнем случае мы не можем использовать синтаксис прямой инициализации, поэтому там лучше всего подходят фигурные скобки.

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

    struct Point < int x = 0; int y = 0; >; setPosition(Point); takeWidget(Widget<>);

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

    setPosition(); takeWidget(<>);

    Но если необходимо просто вызвать конструктор, то лично я предпочитаю использовать прямую инициализацию, то есть классический синтаксис. Я прекрасно понимаю, что в этом со мной многие не согласятся — Николай говорил, что предпочитает для этого использовать фигурные скобки. Мне кажется, что круглые скобки более очевидны, поскольку тогда синтаксис такой же, как при вызове функции, и сразу ясно, что выполняется разрешение перегрузки. Все правила здесь очевидны, не надо думать, есть тут или нет initializer_list . Мне этот подход кажется более простым и ясным: когда смотришь на такой код, сразу ясно, что он делает.

    Ещё раз списком:

    • = value для простых типов
    • = и = <> :
      • для агрегатной инициализации
      • для конструкторов от std::initializer_list
      • для direct member initialisation (с ними нельзя использовать (args) )

      Правда, при использовании (args) мы сталкиваемся с проблемой vexing parse. Но на этот счёт есть ещё один совет. Герб Саттер в 2013 году написал статью, в которой говорилось, что при инициализации нового объекта практически всегда следует использовать auto . Мне этот совет кажется правильным, потому в этом случае все переменные всегда инициализированы: нельзя написать auto i; — это вызовет ошибку компиляции. Если же нужно указать тип, это можно сделать в правой части выражения:

      auto widget = Widget(2, 3);

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

      auto thingy = Thingy();

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

      auto count = std::atomic(0); // C++11/14: ошибка // std::atomic is neither copyable nor movable

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

      auto count = std::atomic(0); // C++17: OK, guaranteed copy elision

      Так что сейчас я советую всегда использовать auto . Единственное исключение — это direct member initializers. Элементы класса с помощью auto объявлять нельзя.

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

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

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

      struct Widget < int a; int b; int c; >; int main() < Widget widget; >;

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

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

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

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

      Widget widget; // ошибка
      Widget widget; // ошибка
      Widget widget; // ошибка
      int arr[3]; // ошибка

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

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

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

      struct Widget < Widget() = delete; int i; int j; >; Widget widget1; // ошибка Widget widget2<>; // работает в C++17, но станет ошибкой в C++20

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

      Было также реализовано ещё одно предложенное мной изменение (wg21.link/p1009). Braced-init-list можно использовать в выражениях new , поэтому часто спрашивают: работают ли они в этих выражениях так же, как при обычной инициализации? Обычно — да, но есть неприятное исключение: braced-init-list не выводит размер в выражениях new :

      double a[]; // OK double* p = new double[]; // ошибка в C++17, заработает в C++20

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

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

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

      struct Widget < int i; int j; >; Widget widget(1, 2); // заработает в C++20

      То есть можно будет писать круглые скобки вместо фигурных для агрегатной инициализации. А это значит, что для агрегатных типов можно будет использовать emplace и make_unique . Это очень важно при написании библиотек. Вновь напомню: всегда используйте auto , то есть предыдущий пример я рекомендовал бы написать следующим образом: 58.11.

      struct Widget < int i; int j; >; auto widget = Widget(1, 2);

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

      int arr[3](0, 1, 2);

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

      Итак, вновь повторим мои рекомендации. Всегда используйте direct member initializers. Всегда пользуйтесь auto . Для вызова конструктора я предпочитаю direct member initializers — мне кажется, это делает код понятнее. Но я понимаю, что многие придерживаются другого мнения по этому вопросу. Так что в конечном итоге выбор за вами — главное, чтобы вы знали все правила.

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

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

      • C++
      • c++ russia
      • c++ russia 2019
      • c++ russia 2019 piter
      • Блог компании JUG Ru Group
      • Программирование
      • C++

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

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