[ English ]

Введение в C++11: auto, decltype, nested templates и range-based for

Этим постом я хочу открыть серию публикаций на тему нововведений в долгожданном недавно принятом стандарте — C++11. Ниже я рассмотрю самые известные и самые простые нововведения, но с каждым постом буду погружаться все глубже и глубже.

Декларация типа с помощью auto

Углубляясь в историю, стоит отметить, что auto переменные были и раньше, до принятия стандарта C++11. Вот только значение они имели другое. Под auto подразумевался спецификатор хранения переменной. То есть, auto находился в одном ряду с register, static, extern, и указывал на то, что переменная имеет локальное время жизни. Об этом почти не знают начинающие, так как любая переменная объявленная в некотором блоке неявно определяется как auto.

Например, следующие два объявления абсолютно идентичны:

void foo()
{
    auto int x = 0;  // явно указывается `auto`
    int y = 0;       // не явно указывается `auto`
}

Стандарт C++11 принес более полезное значение этому ключевому слову. Теперь auto позволяет не указывать тип переменной явно. За определение типа отвечает компилятор, который вычисляет его на основе типа инициализируемого значения.

void foo()
{
    auto x = 5;  // тип переменной x будет int
    x = "foo";   // ошибка! не соответствие типов

    auto y = 4, z = 3.14; // ошибка! нельзя объявлять переменные разных типов
}

Введение auto ужасно повышает читабельность кода, так как теперь нет необходимости писать длинные шаблонные типы. Например, при получении итератора:

// c++03 решение
for (std::vector<std::map<int, std::string>>::const_iterator it = container.begin();
     it != container.end();
     ++it)
{
    // do something
}

// c++11 решение
for (auto it = container.begin(); it != container.end(); ++it)
{
    // do something
}

Что такое decltype и с чем его едят?

decltype позволяет статически определить тип по типу другой переменной.

int x = 5;
double y = 5.1;

decltype(x) foo;    // int
decltype(y) bar;    // double

decltype(x+y) baz;  // double

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

template<class T, class U>
    auto hellSum(const T& x, const U& y) -> decltype(x + y)
    {
        return x + y;
    }

Это лишь пример, и реально в decltype можно использовать результаты функций, функторов и т.д.

>> как закрытие вложенных шаблонов

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

Так или иначе, было справедливо следующее:

std::vector<std::map<int, int>> foo;    // ошибка компиляции
std::vector<std::map<int, int> > foo;   // вполне корректный код

Но с принятием C++11, первый вариант стал тоже корректным и допустимым.

range-based for

Range-Based for — это цикл по контейнеру. Он аналогичен циклу for each в Java или C#. Синтаксически он повторяет for each из Java. Назван он Range-Based в первую очередь потому, чтобы избежать путаницы, ибо в STL уже давно есть алгоритм, именуемый std::for_each.

std::vector<int> foo;

// заполняем вектор

for (int x : foo)
    std::cout << x << std::endl;

Модель ссылок работает также, как и везде:

for (int& x : foo)
    x *= 2;

for (const int& x : foo)
    std::cout << x << std::endl;

Красиво и удобно, правда? Рассмотренный выше auto усиливает данную конструкцию:

std::vector<std::pair<int, std::string>> container;

// ...

for (const auto& i : container)
    std::cout << i.second << std::endl;

Стоит отметить, что хоть range-based for и является мощным и удобным инструментом, он, как и все остальное в C++, не работает на Святом Духе, как могут считать некоторые представители педагогического состава нашей страны. range-based for неявно вызывает у контейнера методы begin() и end(), которые возвращают, в свою очередь, привычные нам итераторы.

Range-Based for, к слову, работает и на обычных статических массивах:

int foo[] = {1, 4, 6, 7, 8};

for (int x : foo)
    std::cout << x << std::endl;

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

Выше я привел лишь малую толику нововведений C++11. Вкусностей гораздо больше, и вы о них узнаете, оставаясь подписчиком моего блога! :)