[ English ]

Введение в C++11: nullptr и нововведения в системе инициализации

Как и обещал, я продолжаю публикации на тему нового стандарта 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), а при создании barFoo(std::initializer_list<int> list). В случае, если последний конструктор будет отсутствовать, то все пойдет как обычно: в обеих случаях вызовется первый конструктор.

Вместо заключения

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