язык — сокращение шаблонного кодаlanguage — boilerplate reduction
_)Placeholder Variables (_)P2169 разрешает использовать идентификатор _ как имя переменной специально в значении "намеренно не используется" — и, в отличие от любого другого идентификатора, _ можно объявлять повторно в одной и той же области видимости без ошибки переопределения.P2169 allows the identifier _ to be used as a variable name specifically meaning "intentionally unused" — and, unlike every other identifier, _ can be redeclared multiple times in the same scope with no redefinition error.
P2169 делает _ особым именем переменной: каждое объявление _ в данной области видимости считается независимым, никак не связанным с предыдущими объявлениями _ в той же области — поэтому auto _ = f(); auto _ = g(); компилируется без ошибки "переопределение", хотя с любым другим именем это была бы ошибка. Это не новое ключевое слово и не зарезервированный символ вообще — это точечное послабление правила "один идентификатор — одно объявление в области видимости" именно и только для одиночного символа подчёркивания.P2169 makes _ a special variable name: every declaration of _ in a given scope is treated as independent, unrelated to previous _ declarations in that same scope — so auto _ = f(); auto _ = g(); compiles with no "redefinition" error, even though it would be one with any other name. This isn't a new keyword or a reserved symbol in general — it's a narrow relaxation of the "one identifier, one declaration per scope" rule specifically and only for the single underscore symbol.
// имена unused1/unused2 или счётчик _1/_2 — сталкиваются при переиспользовании// names like unused1/unused2, or a counter _1/_2 — collide on reuse
[[maybe_unused]] auto unused1 = register_callback();
[[maybe_unused]] auto unused2 = log_event(); // [[maybe_unused]] многословно и не говорит "больше не нужно"// [[maybe_unused]] is verbose and doesn't say "never needed again"
auto _ = register_callback();
auto _ = log_event(); // повторное объявление _ — не ошибка// redeclaring _ is not an error
Паттерн "неиспользуемая переменная" до P2169 закрывался разрозненными, несогласованными соглашениями:The "unused variable" pattern, before P2169, was handled by scattered, inconsistent conventions:
Имя вроде unused или пронумерованные _1/_2/_3 всё равно оставались обычными именами переменных — при повторном использовании в той же области видимости они сталкивались точно так же, как и любое другое имя, требуя ручного отслеживания "какой номер свободен".A name like unused or numbered _1/_2/_3 were still ordinary variable names — reused in the same scope, they collided exactly like any other name, requiring manual tracking of "which number is free."
Префиксация вроде _x — это в некоторых контекстах имя, зарезервированное для реализации, и может провоцировать предупреждения компилятора или линтера, если применяется неаккуратно как пользовательское соглашение.Prefixing with an underscore, like _x, is a name reserved for the implementation in some contexts, and can trigger compiler or linter warnings if applied carelessly as a user-level convention.
Атрибут [[maybe_unused]] подавляет только предупреждение "переменная не используется", но не сообщает "мне структурно не нужно здесь имя" — а ещё требует, чтобы имя всё равно было уникальным. _ даёт единый, повсеместно узнаваемый, не вызывающий предупреждений идиом — давно знакомый пользователям Python, Rust и других языков.The [[maybe_unused]] attribute only suppresses the "variable unused" warning, but doesn't communicate "I structurally don't need a name here" — and still requires the name to be unique. _ gives a single, universally recognized, warning-free idiom — long familiar to users of Python, Rust, and other languages.
_ используется там, где раньше пришлось бы придумывать бессмысленное имя: в структурированном связывании, чтобы отбросить часть кортежа/пары, и как имя параметра функции/лямбды, который не используется в теле. Важно: RAII-объекты вроде std::lock_guard — это ОБРАТНЫЙ случай: guard-переменной либо нужно осмысленное имя, либо вообще никакого (безымянный временный объект) — использование _ для guard'а вводило бы в заблуждение, поскольку весь смысл guard'а именно в побочном эффекте его деструктора, а не в отбрасывании значения.The _ is used where you'd previously have had to invent a meaningless name: in a structured binding, to discard part of a tuple/pair, and as the name of a function/lambda parameter unused in the body. Important: RAII objects like std::lock_guard are the OPPOSITE case: a guard variable either needs a meaningful name or none at all (an unnamed temporary) — using _ for a guard would be misleading, since the entire point of a guard is its destructor's side effect, not discarding a value.
// отбросить часть структурированного связывания// discarding part of a structured binding
auto [_, value] = get_pair();
// _ как неиспользуемый параметр лямбды// _ as an unused lambda parameter
auto handler = [](auto& event, auto& _) { process(event); };
// _ объявлен дважды в одной области видимости — не ошибка// _ declared twice in the same scope — not an error
auto _ = f();
auto _ = g(); // с любым другим именем здесь была бы ошибка переопределения// with any other name, this would be a redefinition error
Когда в цикле нужно только значение, а не ключ — типичнейшее применение _ в структурированном связывании.When a loop only needs the value, not the key — the most typical use of _ in a structured binding.
std::map<std::string, int> my_map = { {"a", 1}, {"b", 2} };
int total = 0;
for (auto& [_, value] : my_map) {
total += value; // ключ структурно не нужен, имя ему давать незачем// the key is structurally unneeded, no reason to name it
}
API вроде std::from_chars возвращает структуру с несколькими полями — если нужен только код ошибки, а не итератор конца, лишнее поле отбрасывается через _.APIs like std::from_chars return a struct with multiple fields — if only the error code is needed, not the end iterator, the extra field is discarded via _.
int value = 0;
auto [_, ec] = std::from_chars(str.data(), str.data() + str.size(), value);
if (ec != std::errc{}) {
// обработка ошибки; итератор конца парсинга структурно не нужен// error handling; the end-of-parse iterator is structurally unneeded
}
Обработчик событий, которому фреймворк передаёт (event, context), но которому нужен только event — контекст отбрасывается через _ прямо в сигнатуре.An event handler the framework calls with (event, context), but which only cares about the event — the context is discarded via _ right in the signature.
event_bus.subscribe([](const Event& event, const Context& _) {
log_event(event.name()); // context нужен сигнатуре подписки, но не этому обработчику// the subscription signature requires context, this handler doesn't
});
Возвращаемое значение неважно, но побочный эффект важен — каждый вызов присваивается в _, что компилируется чисто там, где повторное использование любого другого единственного имени было бы ошибкой переопределения.The return value doesn't matter, but the side effect does — each call is assigned to _, which compiles cleanly where reusing any other single name would be a redefinition error.
auto _ = flush_metrics();
auto _ = notify_watchdog();
auto _ = rotate_log_file(); // три вызова, три раза _, ни одной ошибки переопределения// three calls, three times _, zero redefinition errors
У _ есть настоящее значение, память и время жизни, как у любой другой переменной — оно не исчезает магическим образом, это обычный объект, который просто безымянен по духу, и его деструктор запускается в обычной точке — конце области видимости. Но при ПОВТОРНОМ объявлении _ в той же области видимости до конца этой области "доживает" только ПОСЛЕДНИЙ _: более ранние _-объекты с нетривиальным деструктором уничтожаются немедленно, как только их "затеняет" следующее объявление _, а не в естественной точке конца охватывающей области видимости. Это отличается от обычной семантики затенения, к которой люди привыкли, и стоит проговаривать явно.A _ has a real value, memory, and lifetime like any other variable — it isn't magically elided, it's an ordinary object that just happens to be unnamed in spirit, and its destructor runs at the normal point — the end of scope. But when _ is REDECLARED in the same scope, only the LATEST _ survives to that scope's end: earlier _ objects with a nontrivial destructor are destroyed immediately once "shadowed" by the next _ declaration, not at the natural end of the enclosing scope. This differs from the ordinary shadowing semantics people expect, and is worth calling out explicitly.
_ — не ключевое слово и вообще не зарезервированный идентификатор вне этой конкретной роли placeholder — существующий код, уже использующий _ как обычное имя переменной при одиночном объявлении, продолжает работать без изменений (новая возможность — именно повторное объявление, одиночное объявление _ было легальным всегда).The _ is not a keyword and not reserved outside this specific placeholder role — existing code already using _ as an ordinary variable name in a single-declaration context keeps working unchanged (the new capability is specifically repeated redeclaration; a lone declaration of _ was always legal).
Использование _ исключительно ради подавления предупреждения "unused variable" на переменной, которая на самом деле осмысленно используется дальше — злоупотребление идиомом. _ сообщает читателю кода "это значение сюда больше не вернётся и нигде дальше не читается", а не просто "компилятор перестал ругаться".Using _ purely to silence an "unused variable" warning on something that is actually meaningfully used later is a misuse of the idiom. _ tells the reader "this value is never read again", not merely "the compiler stopped complaining".