Как и обещал, я продолжаю публикации на тему нового стандарта C++11. В прошлой статье я рассказал о таких вещах как:
- автоматическое выведение типов с помощью auto;
- определение типа с помощью decltype;
- закрытие вложенных шаблонов;
- цикл range-based for.
Не много, конечно, но и не мало. Но, как знает читатель, это лишь малая толика того, что дарует нам новый стандарт.
nullptr
В C++11 для обнуления указателей появилось специальное ключевое слово nullptr. В более ранних стандартах, официально использовалась запись:
Foo* foo = 0;
Либо же вариант с макросом NULL, перекочевавшим из языка C. Проблема очевидна: для обнуления указателя используется целое число, из-за чего могут возникать мелкие неприятности. Например, при перегрузке функции:
void func(int x); void func(const Foo* ptr); // ... func(0);
Какой вариант функции вызовется? Очевидно, что первый, принимающий целое число. А мы, может, хотели вызвать второй вариант, только с нулевым указателем. Для решения этой проблемы пришлось бы кастовать 0 к указателю:
func(static_cast<const Foo*>(0));
Но эта ситуация несколько надумана. Признаться, я никогда не встречался с ней в реальном проекте. Но есть более реальная. Допустим, у нас есть некоторый контейнер состоящий из указателей. И мы хотим его обнулить. Вспоминая чудесные алгоритмы из STL, мы не раздумывая применим std::fill.
std::vector<Foo*> foos; // ... std::fill(foos.begin(), foos.end(), 0);
На первый взгляд — все просто отлично! Но ошибка есть, и она такая же как и в предыдущем варианте. std::fill является шаблоном, и увидев 0, шаблон примет его за int и, конечно же, из-за несоответствия типов мы получим очень страшное сообщение об ошибке от компилятора. Выход был такой же — кастовать 0 к указателю, что уж явно не повышает читабельность.
Именно поэтому, было принято новое ключевое слово, и имя ему nullptr. Используя это ключевое слово, мы избавимся от вышеописанной проблемы, так как nullptr имеет свой собственный тип — std::nullptr_t — и компилятор не спутает его ни с чем другим.
Список инициализации
В предыдущем стандарте возможности списков инициализации были чрезвычайно малы. Что мы могли сделать раньше? Лишь проинициализировать некоторую структуру, да некоторый массив:
struct Struct { int x; std::string str; }; // инициализируем атрибуты структуры. Struct s = { 4, "four" }; // инициализируем массив int arr[] = { 1, 8, 9, 2, 4 };
Но C++ предоставляет более удобные, более гибкие средства разработки. Я говорю о классах и контейнерах.
C++11 наконец разрешает эту несправедливость, путем введения шаблонного класса std::initializer_list<>. Все контейнеры отныне обладают конструктором, принимающим список инициализации (std::initializer_list<>), отчего становится реальной следующая запись:
std::vector<int> v = { 1, 5, 6, 0, 9 };
Стоит отметить, что списки инициализации используются не только для инициализации. Например, с помощью последних теперь можно добавлять в контейнер несколько элементов.
std::vector<int> v; v.insert(v.end(), {0, 1, 2, 3, 4});
Разработчик может оборудовать свой класс (в частности контейнер) подобной возможностью. Надо всего лишь определить конструктор принимающий std::initializer_list<>.
class Foo { public: // ... Foo(std::initializer_list<int> list); }; // ... Foo::Foo(std::initializer_list<int> list) { // do something } // ... int x = 5; Foo one = { 1, x, 2, 4, 8 }; Foo two({ 5, 4, 2, x, 4 });
Объекты std::initializer_list<> не могут быть изменены.
Универсальная инициализация
Списки инициализации — это хорошо. Но разработчики решили на этом не останавливаться и пошли еще дальше. Они расширили синтаксис списка инициализации, позволив вытворять следующие вещи:
class Foo { public: // ... Foo(int x, double y, std::string z); }; // ... Foo::Foo(int x, double y, std::string z) { // do something } // ... Foo one = { 1, 2.5, "one" }; Foo two { 5, 3.14, "two" };
Подобная инициализация вызовет конструктор, как будто бы мы написали:
Foo foo(1, 2.5, "one");
Универсальная инициализация работает как для классов, так и для структур. В случае классов вызовется конструктор, а в случае структур — будет происходить поэлементная инициализация в порядке объявления атрибутов.
struct Foo { std::string str; double x; int y; }; Foo foo {"C++11", 4.0, 42}; // {str, x, y} Foo bar {"C++11", 4.0}; // {str, x}, y = 0
Если не указать последний атрибут (или атрибуты), то для него вызовется конструктор по-умолчанию. Для встроенного типа (например int) произойдет инициализация нулем.
Стоит отметить, что такая инициализация позволяет писать следующие вещи:
Foo getFoo() { return { 5, 3.14, "hello" }; } int* foo = new int[5]{0, 1, 2, 3, 4};
Интересен тот факт, что универсальная инициализация защищает от неявных преобразований.
class Foo { public: Foo(int x): _x(x) {} private: int _x; }; // ... Foo foo(3.14); // все ok, double -> int Foo bar{3.14}; // ошибка!
Познакомившись с универсальной инициализацией и списками инициализации, может возникнуть вопрос: "А какой конструктор вызовется в следующей ситуации?"
class Foo { public: Foo(int x, int y) {} Foo(std::initializer_list<int> list) {} }; Foo foo(1, 2); Foo bar{1,2};
В этом случае, при создании объекта foo вызовется конструктор Foo(int x, int y), а при создании bar — Foo(std::initializer_list<int> list). В случае, если последний конструктор будет отсутствовать, то все пойдет как обычно: в обеих случаях вызовется первый конструктор.
Вместо заключения
Хотелось написать больше, но получилось совсем чуть-чуть. Найти время для творчества — это действительно проблема. Ну что же, самые широко известные (за исключением лямбд) нововведения я затронул. В дальнейшем напишу о менее известный вещах.