Возможности C++, о которых должен знать каждый разработчик
С момента своего появления C++ очень хорошо развился как язык программирования.
Конечно, это не произошло моментально. Когда-то этому языку не доставало динамичности. В то время было довольно сложно пользоваться им.
Но всё изменилось, когда комитет по стандартизации C++ решил, что нужно двигаться вперёд.
С 2011 года C++ стал популярным динамическим современным языком программирования.
Не думайте, что язык стал проще. Он до сих пор является одним из самых сложных языков программирования, если не самым сложным из широко используемых. Но, определённо, он стал гораздо проще своих предыдущих версий.
В этой статье мы рассмотрим новые функции этого языка (начиная с C++11, которому уже восемь лет), о которых должен знать каждый разработчик.
Ключевое слово auto
Жизнь стала гораздо проще, когда в C++11 появилось ключевое слово auto
.
Суть этого ключевого слова в том, что оно даёт возможность автоматически определять тип данных на этапе компиляции программы. То есть, вам не нужно каждый раз объявлять, какого типа та или иная переменная. Это очень удобно при объявлении типов данных вроде map<string,vector<pair<int,int>>>
.
auto an_int = 26; //Тип распознаётся компилятором как int
auto a_bool = false; //bool
auto a_float = 26.04; //float
auto ptr = &a_float; //Указатель
auto data; //Разве так можно? Нет.
Обратите внимание на пятую строчку. Вы не можете объявить переменную таким образом без её инициализации. Логично. Компилятор не знает, какой тип данных вы будете использовать для этой переменной.
Изначально возможности auto
были довольно ограничены, но с каждой новой версией языка они расширяются.
auto merge(auto a, auto b) { //Параметры функции и тип, возвращаемый функцией, также могут быть определены при помощи auto
std::vector<int> c = do_something(a, b);
return c;
}std::vector<int> a = {...}; //Какие-то данные
std::vector<int> b = {...}; //Ещё какие-то данные
auto c = merge(a, b); //Тип переменной будет определён в зависимости от аргументов, передаваемых функцией
В седьмой и восьмой строчке при инициализации вектора используются скобки. Это новая функция, добавленная в C++11.
Не забывайте, что при использовании auto
необходимо оставлять подсказки для компилятора для определения типа данных.
А теперь интересный вопрос: что произойдет, если мы напишем auto a = {1, 2, 3}
? Компилятор выдаст ошибку? Или определит тип как vector
?
В 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?
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; //Структурная привязка, о которой я расскажу позже
std::cout << itr.first << " " << v1 << " " << v2 << std::endl;
}
return 0;
}
Обратите внимание на 25 строчку! Выражение 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--; //Так можно делать, потому что мы разрешили лямбда-выражению менять эту переменную
});for(int val: data) {
std::cout << val << ' '; //14 24 20 4 3 6 9
}
В примере выше, если бы вы передали локальную переменную по значению ([factor]
), то в пятой строчке вы не смогли бы изменить её значение. У вас просто-напросто нет прав на это. Помните о своих правах!
Заметьте, что параметр функции val
— ссылка на объект. Таким образом, мы будем уверены в том, что лямбда-выражение действительно изменит вектор.
init внутри 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 {
//Область видимости it распространяется и на else
std::cout << 7 << " is there!" << std::endl;
}
Теперь вы можете инициализировать переменные и проверять условия сразу же внутри блоков if
и switch
. Это помогает сохранять код понятным и чистым. Общий синтаксис таков:
if( init-statement(x); condition(x)) {
// Сделать что-то
}
else { //Внутри else видна переменная x
//Сделать ещё что-то
}
Сделай это при компиляции: constexpr
constexpr
— это классно!
Допустим, у вас есть выражение, значение которого нужно вычислить и которое не будет меняться после этого. Вы можете вычислить значение заранее и использовать его как макрос. Или, как предлагает C++11, вы можете использовать constexpr
.
Программисты сокращают время работы программы насколько возможно. Например, некоторые операции перекладываются на компилятор.
constexpr double fib(int n) { //Функция задекларирована как constexpr
if(n == 1) return 1;
return fib(n - 1) * n;
}int main() {
const long long bigval = fib(20);
std::cout << bigval << std::endl;
}
Код выше — довольно частый пример использования constexpr
.
Так как мы объявили функцию вычисления чисел Фибоначчи как constexpr
, компилятор вычислит значение fib(20)
во время компиляции. Тогда после компиляции строка const long long bigval = fib(20);
будет заменена на const long long bigval = 2432902008176640000;
Заметьте, что передаваемый аргумент имеет квалификатор const
. Это один из немаловажных моментов использования функций с флагом 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 для структурных привязок используется tiestd::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++11.
Вывод параметра шаблона класса
Довольно странное название функции, да? Её суть в том, что с C++17 компилятор может сам определять типы аргументов конструкторов стандартных классов. Раньше же это работало лишь для функций.
std::pair<std::string, int> user = {“M”, 25}; // раньше
std::pair user = {“M”, 25}; // C++17
Для кортежей всё становится ещё проще:
// раньше
std::tuple<std::string, std::string, int> user (“M”, “Chy”, 25);
// предугадывание в действии!
std::tuple user2(“M”, “Chy”, 25);
Для того, чтобы осознать удобство этой функции, нужно быть знакомыми с конструкторами классов в C++.
Умные указатели
Указатели могут быть ужасны.
Из-за свободы, которую вам даёт C++, довольно часто бывает удивительно легко загнать себя в ловушку. И во многих случаях в этом виноваты указатели.
К счастью, в C++11 появились умные указатели. Работать с ними на порядок удобнее, нежели с обычными указателями. Они предотвращают утечку памяти, так как сами освобождают себя, если они не нужны. К тому же они обеспечивают устойчивость к исключениям.
Я хотел бы написать ещё больше об умных указателях, но я не умещу здесь все важные детали.
Это всё на сегодня. С каждой версией в C++ появляются новые функции. Если вам интересно, можете узнать о них больше в этом репозитории.