0

Удивительные возможности современного C++, которые полезно знать каждому разработчику

Обложка: Удивительные возможности современного C++, которые полезно знать каждому разработчику

Было время, когда С++ не хватало динамизма, и увлечься этим языком было трудно. Но всё изменилось, когда было принято решение развить стандарт C++.

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

В статье мы рассмотрим некоторые интересные функциональные возможности языка.

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

Когда в 11 версии C++ только появилось auto, жизнь стала намного легче.

Идея auto состояла в том, чтобы заставить компилятор C++ определять тип ваших данных во время компиляции, вместо того чтобы заставлять вас каждый раз объявлять тип. Это было удобно, если у вас были типы данных вроде map<string, vector <pair <int, int>>> ?

auto an_int = 26; // при компиляции тип выводится в int
auto a_bool = false; // в bool
auto a_float = 26.04f; // в float
auto ptr = &a_float; // и даже в указатель
auto data; // а можно ли так? Вообще-то нельзя.

Посмотрите на строку номер 5. Вы не можете объявить что-либо без инициализатора. Строка 5 не сообщает компилятору, каким может быть тип данных.

Изначально auto было несколько ограничено. Затем, в более поздних версиях языка, у него появилось больше возможностей.

auto merge(auto a, auto b) // Тип параметров и возвращаемых данных тоже может быть auto!
{
	std::vector c = do_something(a, b);
	return c;
}

std::vector<int> a = { ... }; // какие-то данные
std::vector<int> b = { ... }; // какие-то данные
auto c = merge(a, b); // тип определяется возвращаемой информацией!

В строках 7 и 8 была использована инициализация в скобках. Эта функция также была добавлена в 11 версии C++.

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

Теперь встаёт хороший вопрос, что произойдёт, если мы напишем auto a = {1, 2, 3}? Это ошибка компиляции? Это вектор?

 

На самом деле, в 11 версии C++ был представлен 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::pait<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 — новая функция в 17 версии C++. Это называется структурным связыванием. В предыдущих версиях приходилось извлекать каждую переменную отдельно. Но структурное связывание сделало этот процесс более удобным.

Более того, если вы хотите получить данные, используя ссылку, то просто добавьте символ — auto &[v1, v2] = itr.second.

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

В 11 версии C++ появились лямбда-выражения. Это что-то вроде анонимных функций в 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(), которые тоже были добавлены в 11 версии. После идёт лямбда-выражение в качестве компаратора ваших данных. Параметры лямбда-выражения объявлены с помощью auto, что было добавлено в 14 версии С++. До этого auto нельзя было использовать в качестве параметров функции.

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

Как определено в этом потрясающем репозитории по современному C++:

  • [ ] — ничего не захватывает. Таким образом, вы не можете использовать любую локальную переменную внешней области видимости в лямбда-выражении. Вы можете использовать только параметры.
  • [=] — захватывает локальные объекты (локальные переменные, параметры) в области видимости по значению. Вы можете использовать, но не изменять их.
  • [&] — захватывает локальные объекты (локальные переменные, параметры) в области видимости по ссылке. Вы можете изменить их, как в примере, приведённом ниже.
  • [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]) в лямбда-выражении, то вы не можете изменить factor в 5 строке. Вы просто не имеете права делать это. Не злоупотребляйте своими правами!

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

Инициализатор в if и switch

Вам точно понравится эта возможность в С++ 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

Скажем, у вас есть какое-то выражение для оценки, и его значение не изменится после инициализации. Вы можете предварительно рассчитать значение, а затем использовать его в качестве макроса. Или, как предложил 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;
	return 0;
}

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

Поскольку мы объявили функцию вычисления Фибоначчи как constexpr, компилятор может предварительно вычислить fib(20) во время компиляции. Так что после неё он может заменить строку с

const long long bigval = fib (20);

на

const long long bigval = 2432902008176640000;

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

Переменные также могут быть constexpr. В этом случае, как вы можете догадаться, эти переменные должны вычисляться во время компиляции. Иначе вы получите ошибку компиляции.

Интересно, что позже в C++ 17 были представлены constexpr-if и constexpr-lambda.

Кортежи

Как и пара, кортеж представляет собой набор значений фиксированного размера для различных типов данных.

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);

// в 11 версии С++ мы использовали tie, чтобы сделать связывание

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

// но в 17 версии стало гораздо удобнее
auto [first_name, last_name, age] = user_info;

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

Вывод типов шаблонных параметров для классов

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

std::pair<std::string, int> user = {"M", 25}; // раньше
std::pair user = {"M", 25}; // C++ 17

Прим. перев. В этом примере для первого элемента кортежа будет выведен тип const char *, а не std::string.

Выводимый тип задаётся неявно. Это становится ещё удобнее для кортежей.

// раньше
std::tuple<std::string, std::string, int> user ("M", "Chy", 25); 
// C++ 17
std::tuple user2("M", "Chy", 25);

Эта функция не имеет никакого смысла, если вы слабо знакомы с шаблонами в C++.

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

Указатели могут быть адскими.

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

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

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

Перевод статьи Some awesome modern C++ features that every developer should know

Варвара Николаева