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
, поэтому нужно привязывать все значения к именованным переменным. Это может оказаться неэффективным, если позже не задействовать некоторые переменные, но тем не менее компилятор может оптимизировать неиспользованное связывание.