глобально статические объекты в библиотеках, указанных в заголовочных файлах, благодаря новой возможности объявлять встраиваемые переменные, что ранее было выполнимо только для функций.
Отдельные примеры данной главы могут оказаться более интересными для тех, кто реализует библиотеки, нежели для тех, кто пишет приложения. Для полноты картины мы рассмотрим несколько свойств, но вам не обязательно разбираться со всеми примерами главы прямо сейчас, чтобы понять остальной материал этой книги.
Применяем структурированные привязки (декомпозицию) для распаковки набора возвращаемых значений
В C++17 появилась новая возможность, объединяющая синтаксический сахар и автоматическое определение типа, — структурированные привязки. Эта функция помогает присваивать отдельные значения пар, кортежей и структур отдельным переменным. В других языках программирования этот механизм называется распаковкой.
Как это делается
Применение декомпозиции для присвоения значений нескольким переменным на основе одной упакованной структуры всегда выполняется за один шаг. Сначала рассмотрим, как это делалось до появления С++17. Затем взглянем на несколько примеров, в которых показаны способы воплощения этого в С++17.
1. Получаем доступ к отдельным значениям std::pair. Представьте, что у нас есть математическая функция divide_remainder, которая принимает в качестве параметров делимое и делитель и возвращает частное и остаток в std::pair.
std::pair<int, int> divide_remainder(int dividend, int divisor);
Рассмотрим следующий способ получения доступа к отдельным значениям полученной пары.
const auto result (divide_remainder(16, 3));
std::cout << "16 / 3 is "
<< result.first << " with a remainder of "
<< result.second << 'n';
Вместо выполнения действий, показанных во фрагменте выше, мы теперь можем присвоить отдельные значения конкретным переменным с говорящими именами, что более удобочитаемо:
auto [fraction, remainder] = divide_remainder(16, 3);
std::cout << "16 / 3 is "
<< fraction << " with a remainder of "
<< remainder << 'n';
2. Структурированные привязки работают и для std::tuple. Рассмотрим следующий пример функции, которая возвращает информацию о ценах на акции:
std::tuple<std::string,
std::chrono::system_clock::time_point, unsigned>
stock_info(const std::string &name);
Присваивание результата ее работы отдельным переменным выглядит так же, как и в предыдущем примере:
const auto [name, valid_time, price] = stock_info("INTC");
3. Декомпозицию можно применять и для пользовательских структур. В качестве примера создадим следующую структуру.
struct employee {
unsigned id;
std::string name;
std::string role;
unsigned salary;
};
Теперь можно получить доступ к ее членам с помощью декомпозиции. Мы даже можем сделать это в цикле, если предполагается наличие целого вектора таких структур:
int main()
{
std::vector<employee> employees {
/* Инициализируется в другом месте */};
for (const auto &[id, name, role, salary] : employees) {
std::cout << "Name: " << name
<< "Role: " << role
<< "Salary: " << salary << 'n';
}
}
Как это работает
Структурированные привязки всегда применяются по одному шаблону:
auto [var1, var2, ...] = <выражение пары, кортежа, структуры или массива>;
□ Количество переменных var1, var2... должно точно совпадать с количеством переменных в выражении, в отношении которого выполняется присваивание.
□ Элементом <выражение пары, кортежа, структуры или массива> должен быть один из следующих объектов:
• std::pair;
• std::tuple;
• структура. Все члены должны быть нестатическими и определенными в одном базовом классе. Первый объявленный член присваивается первой переменной, второй член — второй переменной и т.д.;
• массив фиксированного размера.
□ Тип может иметь модификаторы auto, const auto, const auto& и даже auto&&.
При необходимости пользуйтесь ссылками, а не создавайте копии. Это важно не только с точки зрения производительности.
Если в квадратных скобках вы укажете слишком мало или слишком много переменных, то компилятор выдаст ошибку.
std::tuple<int, float, long> tup {1, 2.0, 3};
auto [a, b] = tup; // Не работает
В этом примере мы пытаемся поместить кортеж с тремя переменными всего в две переменные. Компилятор незамедлительно сообщает нам об ошибке:
error: type 'std::tuple<int, float, long>' decomposes into 3 elements,
but only 2 names were provided
auto [a, b] = tup;
Дополнительная информация
С помощью структурированных привязок вы точно так же можете получить доступ к большей части основных структур данных библиотеки STL. Рассмотрим, например, цикл, который выводит все элементы контейнера std::map:
std::map<std::string, size_t> animal_population {
{"humans", 7000000000},
{"chickens", 17863376000},
{"camels", 24246291},
{"sheep", 1086881528},
/* … */
};
for (const auto &[species, count] : animal_population) {
std::cout << "There are " << count << " " << species
<< " on this planet.n";
}
Пример работает потому, что в момент итерации по контейнеру std::map мы получаем узлы std::pair<const key_type, value_type> на каждом шаге этого процесса. Именно эти узлы распаковываются с помощью структурированных привязок (key_type представляет собой строку с именем species, а value_type — переменную count типа size_t), что позволяет получить к ним доступ по отдельности в теле цикла.
До появления C++17 аналогичного эффекта можно было достичь с помощью std::tie:
int remainder;
std::tie(std::ignore, remainder) = divide_remainder(16, 5);
std::cout << "16 % 5 is " << remainder << 'n';
Здесь показано, как распаковать полученную пару в две переменные. Применение контейнера std::tie не так удобно, как использование декомпозиции, ведь нам надо заранее объявить все переменные, которые мы хотим связать. С другой стороны, пример демонстрирует преимущество std::tie перед структурированными привязками: значение std::ignore играет роль переменной-пустышки. В данном случае частное нас не интересует и мы отбрасываем его, связав с std::ignore.
Когда мы применяем декомпозицию, у нас нет переменных-пустышек tie, поэтому нужно привязывать все значения к именованным переменным. Это может оказаться неэффективным, если позже не задействовать некоторые переменные, но тем не менее компилятор может оптимизировать неиспользованное связывание.
Раньше функцию divide_remainder можно было реализовать следующим образом, используя выходные параметры:
bool divide_remainder(int dividend, int divisor,
int &fraction, int &remainder);