Головна ілюзія structured binding: запис
const auto& [a, b] = get_pair();
часто сприймають як оголошення двох незалежних посилань a і b. Це не так.
Компілятор створює одну приховану змінну, застосовує всі модифікатори (const, &, &&) лише до неї, а імена в дужках робить спеціальними псевдонімами.
Як розгортається оголошення
cv-auto& [x, y] = expr;
Крок 1. Прихована змінна:
cv-auto& __e = expr;
Специфікатори (const, volatile, & / &&, атрибути на кшталт [[maybe_unused]] на рівні оголошення) стосуються __e, а не x / y окремо.
Крок 2. Ідентифікатори x і y — structured binding identifiers. Залежно від типу __e:
Тип __e |
Що означають x, y |
|---|---|
| Масив | __e[0], __e[1] |
Tuple-like (std::tuple, std::pair, …) |
через std::tuple_element і get<i>(__e) |
| Структура / клас з відкритими полями | прямі звернення до членів |
Підступ 1: decltype не дає «видимий» тип посилання
struct Point { int x; int y; };
Point pt{10, 20};
auto& [a, b] = pt;
Інтуїція: decltype(a) → int& (бо написали auto&).
Реальність: int.
static_assert(std::is_same_v<decltype(a), int>); // ок
static_assert(std::is_same_v<decltype(a), int&>); // ні
Правило: для ідентифікатора зв’язування decltype повертає тип поля (або tuple_element), а не тип посилання, яким прив’язаний об’єкт.
Модифікація все одно йде в оригінал, бо доступ іде через поля посилальної __e:
a = 99; // pt.x == 99
Щоб отримати тип виразу доступу з урахуванням категорії значення, потрібні подвійні дужки:
decltype((a)) // int&
Підступ 2: час життя і посилання всередині
struct Data { std::string text; };
Data get_data() { return Data{"Hello World"}; }
const auto& [t1] = get_data();
// Безпечно щодо тимчасового Data: lifetime extension на область видимості прихованої змінної.
Інша пастка — коли всередині повернутого об’єкта вже висять посилання на локальні:
std::pair<const std::string&, int> get_ref_pair() {
std::string local = "danger";
return {local, 1}; // local знищується на виході — UB у самому return
}
auto [s, val] = get_ref_pair();
// structured binding не «лікує» посилання на знищений рядок
Підступ 3: const і сирі покажчики
const auto& робить константною приховану змінну (і псевдоніми полів), але не дані за сирим покажчиком:
struct Node { int* ptr; };
int value = 42;
Node node{&value};
const auto& [p] = node;
// __e: const Node&; p відповідає int* const (з погляду доступу до поля)
*p = 100; // валідно: змінюються дані за адресою, не сам покажчик як поле
Підступ 4: специфікатори не на окремі імена
Не можна взяти одне поле за посиланням, а інше скопіювати в одному списку:
// помилка синтаксису:
// auto [&a, b] = get_pair();
auto [a, b] = get_pair(); // обидва за правилами копії з __e
auto& [ar, br] = get_pair(); // обидва через посилальну __e
auto b_copy = br; // явна копія за потреби
C++20
Захоплення ідентифікаторів structured binding у лямбду в C++17 було заборонено; у C++20 дозволено:
auto [id, score] = get_student();
auto print = [id, score]() {
std::cout << id << ": " << score << '\n';
};
Також можна маркувати окремі імена атрибутами, наприклад [[maybe_unused]].
Адаптація власного класу через tuple-like протокол — у наступній нотатці.