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

Обложка поста

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

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

Было время, когда С++ не хватало динамизма, и увлечься этим языком было трудно. Но всё изменилось, когда было принято решение развить стандарт 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 появились умные указатели, которые намного удобнее, чем простые. Они помогают программистам предотвращать утечки памяти, освобождая её, когда это возможно. Они также обеспечивают исключительную безопасность.

Не смешно? А здесь смешно: @ithumor