Автор материала, перевод которого мы сегодня публикуем, говорит, что C++, в его современном виде, если сравнивать его с тем, чем был этот язык несколько лет назад, значительно изменился в лучшую сторону. Конечно, эти изменения произошли далеко не сразу. Например, в былые времена C++ не хватало динамичности. Непросто было найти человека, который мог бы сказать, что он питает к этому языку нежные чувства. Всё изменилось тогда, когда те, кто отвечает за стандартизацию языка, решили дать ход новшествам. В 2011 году C++ стал динамическим языком, языком, который постоянно развивается и вызывает у программистов куда больше положительных эмоций.

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



Сегодня мы поговорим о некоторых новых возможностях языка (начиная с C++ 11, которому, кстати, уже 8 лет), знать о которых будет полезно любому программисту.

Ключевое слово auto


С тех пор, как в C++ 11 появилось ключевое слово auto, жизнь программистов стала легче. Благодаря этому ключевому слову компилятор может выводить типы переменных во время компиляции, что избавляет нас от необходимости всегда самостоятельно указывать типы. Это оказалось очень удобным, например, в тех случаях, когда приходится работать с типами данных наподобие map<string,vector<pair<int,int>>>. При использовании ключевого слова auto нужно учитывать некоторые особенности. Рассмотрим пример:

auto an_int = 26; // во время компиляции выясняется, что тип этой переменной - int
auto a_bool = false; // выводится тип bool
auto a_float = 26.04; // выводится тип float
auto ptr = &a_float; // работает этот механизм даже с указателями
auto data; // #1 а можно ли поступить так? На самом деле - нет.

Обратите внимание на строку, последнюю в этом примере, комментарий к которой отмечен как #1 (здесь и далее подобным образом мы будем отмечать строки, которые будем, после примеров, разбирать). В этой строке нет инициализатора, так делать нельзя. Код, расположенный в этой строке, не позволяет компилятору узнать о том, каким должен быть тип соответствующей переменной.

Изначально возможности ключевого слова auto в C++ было довольно-таки ограниченными. Затем, в более свежих версиях языка, возможностей у auto добавилось. Вот ещё один пример:

auto merge(auto a, auto b) // при объявлении параметров функций и при указании возвращаемых типов можно использовать auto!
{
    std::vector<int> c = do_something(a, b);
    return c;
}

std::vector<int> a = { ... }; // #1 какие-то данные
std::vector<int> b = { ... }; // #2 ещё какие-то данные
auto c = merge(a,b); // тип выведен на основании полученных данных

В строках #1 и #2 применяется инициализация переменной с использованием фигурных скобок — ещё одна новая возможность C++ 11.

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

Теперь — интересный вопрос. Что случится, если воспользоваться конструкцией наподобие auto a = {1, 2, 3}? Что это? Вектор, или повод для ошибки компиляции?

На самом деле, в C++ 11 появилась конструкция вида std::initializer_list<type>. Находящийся в скобках список инициализационных значений будет рассматриваться как контейнер при использовании ключевого слова auto.

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

void populate(auto &data) { // только посмотрите на это!
    data.insert({"a",{1,4}});
    data.insert({"b",{3,1}});
    data.insert({"c",{2,3}});
}

auto merge(auto data, auto upcoming_data) { // не нужно снова описывать длинные объявления
    auto result = data;
    for(auto it: upcoming_data) {
        result.insert(it);
    }
    return result;
}

int main() {
    std::map<std::string, std::pair<int,int>> data;
    populate(data);

    std::map<std::string, std::pair<int,int>> upcoming_data;
    upcoming_data.insert({"d",{5,3}});

    auto final_data = merge(data,upcoming_data);

    for(auto itr: final_data) {
        auto [v1, v2] = itr.second; // #1 о декомпозиции при объявлении переменных мы поговорим ниже
        std::cout << itr.first << " " << v1 << " " << v2 << std:endl;
    }
    return 0;
}

Взгляните на строку #1. Выражение auto [v1,v2] = itr.second представляет собой новую возможность C++ 17. Это — так называемая декомпозиция при объявлении переменных. В предыдущих версиях языка нужно было извлекать каждое значение по-отдельности. Благодаря этому механизму выполнять подобные операции стало гораздо удобнее.

Более того, если нужно работать с данными, используя ссылки, в эту конструкцию достаточно добавить всего один символ, преобразовав её к следующему виду: auto &[v1,v2] = itr.second.

Лямбда-выражения


В C++ 11 появилась поддержка лямбда-выражений. Они напоминают анонимные функции в JavaScript, их можно сравнить с функциональными объектами без имён. Они захватывают переменные в различных областях видимости в зависимости от их описания, для которого используются компактные синтаксические конструкции. Кроме того, их можно назначать переменным.

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

std::vector<std::pair<int,int>> data = {{1,3}, {7,6}, {12, 4}}; // обратите внимание на инициализацию с использованием скобок
std::sort(begin(data), end(data), [](auto a, auto b) { // и снова - auto!
    return a.second < b.second;
});

В этом кратком примере можно найти много интересного.

Для начала — обратите внимание на то, как удобно пользоваться инициализацией переменной с использованием фигурных скобок. Далее мы можем видеть стандартные конструкции begin() и end(), которые тоже появились в C++ 11. Затем идёт лямбда-функция, используемая в качестве механизма для сравнения данных. Параметры этой функции объявлены с помощью ключевого слова auto, данная возможность появилась в C++ 14. Ранее это ключевое слово нельзя было использовать при описании параметров функций.

Теперь обратите внимание на то, что лямбда-выражение начинается с квадратных скобок — []. Это — так называемая маска переменных. Она определяет область видимости выражения, то есть позволяет управлять взаимоотношениями лямбда-выражения с локальными переменными и объектами.

Вот выдержка из этого репозитория, посвящённого современным возможностям C++:

  • [] — выражение ничего не захватывает. Это значит, что в лямбда-выражении нельзя использовать локальные переменные из внешней по отношению к нему области видимости. В выражении можно использовать лишь параметры.
  • [=] — выражение захватывает значения локальных объектов (то есть — локальные переменные, параметры). Это значит, что их можно использовать, но не модифицировать.
  • [&] — выражение захватывает ссылки на локальные объекты. Их можно модифицировать, это показано в следующем примере.
  • [this] — выражение захватывает значение указателя this.
  • [a, &b] — выражение захватывает значение объекта a и ссылку на объект b.

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

std::vector<int> data = {2, 4, 4, 1, 1, 3, 9};
int factor = 7;
for_each(begin(data), end(data), [&factor](int &val) { // получаем доступ к factor по ссылке
    val = val * factor;
    factor--; // #1 это возможно из-за того, что лямбда-выражение получает доступ к factor по ссылке
});

for(int val: data) {
    std::cout << val << ' '; // 14 24 20 4 3 6 9
}

Здесь, если бы доступ к переменной factor осуществлялся бы по значению (тогда при описании лямбда-выражения использовалась бы маска переменных [factor]), то в строке #1 значение factor менять было бы нельзя — просто потому что у нас не было бы прав на выполнение такой операции. В данном же примере право на подобные действия у нас есть. В таких ситуациях важно не злоупотреблять возможностями, которые даёт доступ к переменным по ссылке.

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

Выражения инициализации переменных внутри конструкций if и switch


Это новшество C++ 17 очень понравилось мне сразу после того, как я о нём узнал. Рассмотрим пример:

std::set<int> input = {1, 5, 3, 6};

if(auto it = input.find(7); it==input.end()){ // первая часть - инициализация, вторая - условие
    std::cout << 7 << " not found" << std:endl;
}
else {
    // у этого блока else есть доступ к области видимости it
    std::cout << 7 << " is there!" << std::endl;
}

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

if( init-statement(x); condition(x)) {
    // выполнить некие действия
} else {
    // здесь можно работать с x
    // выполнить некие действия
}

Выполнение вычислений во время компиляции с использованием constexpr


Ключевое слово constexpr даёт нам замечательные возможности. Предположим, у нас есть некое выражение, которое надо вычислить, при этом его значение, после инициализации им соответствующей переменной, меняться не будет. Такое выражение можно вычислить заранее и использовать его как макрос. Или, что стало возможным в C++ 11, воспользоваться ключевым словом constexpr.

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

#include <iostream>

constexpr long long fact(long long n) { // функция объявлена с использованием ключевого слова constexpr
    return n == 1 ? 1 : (fact(n-1) * n);
}

int main()
{
    const long long bigval = fact(20);
    std::cout<<bigval<<std::endl;
}

Это — весьма распространённый пример использования constexpr.

Так как мы объявили функцию для вычисления факториала как constexpr, компилятор может заранее вычислить значение fact(20) во время компиляции программы. В результате, после компиляции строку const long long bigval = fact(20); можно будет заменить на const long long bigval = 2432902008176640000;.

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

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

Интересно отметить, что позже, в C++ 17, появились конструкции constexpr-if и constexpr-lambda.

Структуры данных tuple


Так же как и структура данных pair, структура данных tuple (кортеж) представляет собой коллекцию значений разных типов фиксированного размера. Вот пример:

auto user_info = std::make_tuple("M", "Chowdhury", 25); // используем auto для автоматического вывода типа

// доступ к данным
std::get<0>(user_info);
std::get<1>(user_info);
std::get<2>(user_info);

// в C++ 11 для выполнения привязок использовали tie

std::string first_name, last_name, age;
std::tie(first_name, last_name, age) = user_info;

// в C++ 17, конечно, то же самое делается гораздо удобнее
auto [first_name, last_name, age] = user_info;

Иногда вместо структуры данных tuple удобнее использовать std::array. Эта структура данных похожа на простые массивы, используемые в языке C, снабжённые дополнительными возможностями из стандартной библиотеки C++. Эта структура данных появилась в C++ 11.

Автоматический вывод типа аргумента шаблона класса


Выглядит название этой возможности довольно-таки длинным и сложным, но на самом деле ничего сложного тут нет. Основная идея тут заключается в том, что в C++ 17 вывод типов аргументов шаблонов выполняется и для стандартных шаблонов классов. Ранее это поддерживалось лишь для функциональных шаблонов. В результате оказывается, что раньше писали так:

std::pair<std::string, int> user = {"M", 25};

С выходом C++ 17 эту конструкцию стало можно заменить на эту:

std::pair user = {"M", 25};

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

std::tuple<std::string, std::string, int> user ("M", "Chy", 25);

Теперь же то же самое выглядит так:

std::tuple user2("M", "Chy", 25);

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

Умные указатели


Работа с указателями в C++ может оказаться настоящим кошмаром. Благодаря той свободе, которую язык даёт программисту, порой ему бывает очень непросто, как говорится, «не выстрелить себе в ногу». Во многих случаях к такому вот «выстрелу» программиста подталкивают именно указатели.

К нашему счастью, в C++ 11 появились умные указатели, которые гораздо удобнее обычных. Они помогают программисту избегать утечек памяти, освобождая, когда это возможно, ресурсы. Кроме того, они дают гарантию безопасности по исключениям.

Итоги


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

Уважаемые читатели! Какие современные возможности C++ кажутся вам самыми интересными и полезными?

Комментарии (12)


  1. Nomad1
    17.05.2019 13:23
    -1

    бывает очень непросто, как говорится, «не выстрелить себе в ногу»
    И с каждым годом выстрелить себе в ногу можно все более необычным методом :)

    мысли вслух
    Не поймите неправильно, я обожаю С++, особенно когда на нем написаны адекватные и красивые вещи. И расстраиваюсь, когда приходится разбираться в подобном коде:
     PositionType& posAt(PositionType pos) {
            return posBuffer[pos.y * input.getWidth() + pos.x];
        }
    
        void transformAt(PositionType pos, PositionType target /* ... */) {
            target += pos;
            /* ... bla-bla ... */
                posAt(pos) = target = posAt(target);
                setPixel<OutputType>(pos, std::sqrt(square(pos.x - target.x) + square(pos.y - target.y)));
            /* ... bla-bla ... */
        }
    


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


  1. Ryppka
    17.05.2019 13:44

    auto a_float = 26.04; // выводится тип float

    Вроде по стандарту литерал с плавающей точкой имеет тип double?


    1. luciys
      17.05.2019 14:41

      Да

      И в статье ошибки с выводом типов шаблонов.

      В результате оказывается, что раньше писали так:

      std::pair<std::string, int> user = {"M", 25};


      С выходом C++ 17 эту конструкцию стало можно заменить на эту:

      std::pair user = {"M", 25};



      Неправильно заменять, т.к. тип будет другим:
      std::pair<const char*, int>

      1. Мы не сможем изменять эту строку.
      2. Сама строка «М» будет храниться статически.


  1. NickViz
    17.05.2019 14:32

    >> std::pair user = {«M», 25};
    а почему строковый литерал внезапно станет std::string?


  1. neurocore
    17.05.2019 14:43

    Да много чего интересного есть в новых версиях, можно упомянуть обобщённый функциональный объект function<>, классы свойств trait, перегрузку операторов для произвольных типов (не объектов класса)


  1. Serge78rus
    17.05.2019 14:51
    +1

    К нашему счастью, в C++ 11 появились умные указатели, которые гораздо удобнее обычных.
    Они появились гораздо раньше, а к моменту появления C++ 11 первая их реинкарнация в виде auto_ptr уже успела устареть и объявлена deprecated


  1. Jeka178RUS
    17.05.2019 15:30

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

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


  1. kovserg
    17.05.2019 23:21
    +1

    Главное что надо знать всем программистам:

    Последние нововведения в C++ были созданы, чтобы исправить предыдущие нововведения.


  1. da-nie
    18.05.2019 09:53
    +1

    Но современный C++ стал гораздо дружелюбнее, чем раньше.


    А, так вот почему я так и не могу до сих пор сказать, что знаю Си++. :)


  1. ktod
    18.05.2019 18:07

    Как я в свое время протащился от появления в плюсах: лямбд, замыканий и списка инициализации!
    Кстати, в статье не хватает параграфа о mutable, имхо.


  1. Dmitri-D
    19.05.2019 07:38

    похоже автор как-то очень сильно ограничился.

    а как же variadic templates?
    а как же make_tuple?
    а как же decltype?
    а как же эта масса новых полезных шаблонов в STL?
    а как же unordered_map и unordered_set?
    а как же move semantic?


    1. Ryppka
      19.05.2019 18:48

      Чем «меньше» фича, тем она реально полезнее? )))