Автор материала, перевод которого мы сегодня публикуем, говорит, что 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++ кажутся вам самыми интересными и полезными?
Автор: ru_vds