Головна ілюзія 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 протокол — у наступній нотатці.