ядро языкаcore language
Явное decay-копирование выражения — создать независимую копию того же типа, что вывел бы auto, без промежуточной именованной переменной.An explicit decay-copy of an expression — creating an independent copy of the type auto would have deduced, without an intermediate named variable.
auto(x) и auto{x} создают временный объект, тип которого выводится по тем же правилам, что и для переменной, объявленной как auto (см. cpp11/auto.html) — то есть без ссылок и без верхнеуровневых cv-квалификаторов, даже если исходное выражение x было ссылкой на const. Результат — независимая, изменяемая копия значения, полученная прямо в месте использования выражения, без объявления промежуточной переменной.auto(x) and auto{x} create a temporary object whose type is deduced by the same rules as for a variable declared with auto (see cpp11/auto.html) — that is, with no references and no top-level cv-qualifiers, even if the original expression x was a reference to const. The result is an independent, mutable copy of the value, obtained right at the point of use, with no intermediate variable declared.
const std::string& get_ref();
void use_copy() {
auto copy = get_ref(); // требуется имя переменной, отдельная строка// needs a variable name, a separate line
process(copy);
// либо многословно прямо в выражении:// or, verbosely, right in the expression:
process(std::decay_t<decltype(get_ref())>(get_ref()));
}
const std::string& get_ref();
void use_copy() {
process(auto(get_ref())); // decay-копия прямо на месте, без имени// a decay-copy right there, with no name
}
Идиома "форсировать decay-копию значения" встречается достаточно часто, чтобы заслужить собственный лаконичный синтаксис — до C++23 все доступные способы были неудобны хотя бы в одном отношении:The "force a decay-copy of a value" idiom comes up often enough to deserve its own concise syntax — before C++23, every available approach was inconvenient in at least one respect:
auto copy = x; — самый очевидный способ, но он требует придумать имя для переменной, которая часто нужна ровно один раз, сразу передаётся куда-то дальше, и не несёт самостоятельной смысловой роли — лишняя строка кода и лишняя сущность в области видимости ради одной операции копирования.auto copy = x; is the most obvious approach, but it requires inventing a name for a variable that's often used exactly once, gets immediately passed onward, and carries no independent semantic role — an extra line of code and an extra entity in scope for the sake of a single copy operation.
Конструкция std::decay_t<decltype(x)>(x) достигает того же результата без именованной переменной, но требует повторить выражение x дважды (один раз внутри decltype, второй раз как аргумент конструктора) и явно подключить <type_traits> — многословно и подвержено ошибкам при рефакторинге, если x — сложное выражение, которое меняется в одном месте, но забывается в другом.The std::decay_t<decltype(x)>(x) construct achieves the same result without a named variable, but requires repeating the expression x twice (once inside decltype, once as the constructor argument) and explicitly including <type_traits> — verbose and error-prone during refactoring if x is a complex expression that gets changed in one place but forgotten in the other.
В шаблонном коде тип x часто неизвестен явно — писать std::decay_t<decltype(x)> руками для каждого такого места избыточно, когда auto уже умеет выводить нужный тип. В композиции с ranges-адаптерами и лямбдами часто нужно "форсировать копию" элемента прямо внутри выражения (например, чтобы гарантированно захватить значение, а не ссылку на временный прокси-объект range), не вынося отдельную переменную наружу.In template code the type of x is often not explicitly known — writing std::decay_t<decltype(x)> by hand for every such spot is redundant when auto already knows how to deduce the right type. Composed with ranges adaptors and lambdas, you often need to "force a copy" of an element right inside an expression (e.g. to guarantee capturing a value rather than a reference into a range's temporary proxy object), without pulling out a separate variable.
Формально auto(x) — это "явное decay-копирующее преобразование" (explicit decay-copy conversion): компилятор сначала применяет к x те же правила вывода типа, что применил бы к инициализатору переменной, объявленной auto (снимает ссылочность и верхнеуровневые const/volatile, массивы распадаются в указатели, функции — в указатели на функции), а затем строит временный объект этого выведенного типа, используя x как единственный аргумент конструктора.Formally, auto(x) is an "explicit decay-copy conversion": the compiler first applies to x the same type-deduction rules it would apply to the initializer of a variable declared with auto (stripping references and top-level const/volatile, decaying arrays to pointers, functions to function pointers), then constructs a temporary object of that deduced type, using x as the sole constructor argument.
void demo(const std::vector<int>& ref) {
auto copy1 = auto(ref); // copy1: std::vector<int>, не const, не ссылка// copy1: std::vector<int>, not const, not a reference
copy1.push_back(42); // можно изменять — ref остаётся нетронутым// mutable — ref itself stays untouched
modify_in_place(auto(ref)); // временная decay-копия передаётся напрямую, без имени// a temporary decay-copy passed directly, with no name
}
Разница между auto(x) и auto{x} зеркально повторяет разницу между обычной функциональной инициализацией T(x) и списочной инициализацией T{x}: для большинства типов результат идентичен, но для типов с конструктором, принимающим std::initializer_list, списочная форма может выбрать именно этот конструктор там, где круглые скобки выбрали бы другой.The difference between auto(x) and auto{x} mirrors the difference between ordinary functional-style initialization T(x) and list initialization T{x}: for most types the result is identical, but for types with a constructor taking std::initializer_list, the brace form may pick exactly that constructor where the parenthesized form would have picked a different one.
std::vector<int> v = {1, 2, 3};
auto a = auto(v.size()); // auto(x) на size_t: обычное копирование числа// auto(x) on a size_t: ordinary copying of a number
auto b = auto{v.size()}; // auto{x} на size_t: тоже число, разницы здесь нет// auto{x} on a size_t: also a number, no difference here
// разница проявляется, только если тип имеет initializer_list-конструктор —// the difference only shows up if the type has an initializer_list constructor —
// см. классическую ловушку vector<int> v(5) vs v{5} в разделе "грабли"// see the classic vector<int> v(5) vs v{5} trap in the gotchas section
Функция, которая обязана вернуть независимую копию внутреннего поля, а не ссылку — auto(x) делает намерение "это копия, не ссылка" явным прямо в точке возврата, без дополнительной переменной.A function that must return an independent copy of an internal field, not a reference — auto(x) makes the "this is a copy, not a reference" intent explicit right at the return point, with no extra variable.
class Cache {
public:
std::string snapshot() const {
return auto(current_value_); // явная копия, даже если бы current_value_ был ссылкой// an explicit copy, even if current_value_ were a reference
}
private:
std::string current_value_;
};
Range-адаптеры вроде views::transform нередко возвращают прокси-ссылки на временные объекты — auto(x) внутри лямбды форсирует независимую копию элемента прямо в точке использования, без риска захватить ссылку на то, что перестанет существовать.Range adaptors like views::transform often return proxy references to temporaries — auto(x) inside a lambda forces an independent copy of the element right at the point of use, with no risk of capturing a reference to something that's about to stop existing.
auto snapshots = data
| std::views::transform([](const auto& item) {
return auto(item.value); // независимая копия, не ссылка на элемент диапазона// an independent copy, not a reference into the range
});
Шаблонная функция, принимающая forwarding-параметр, которой в одном месте нужна независимая копия значения (например, чтобы безопасно положить в контейнер, переживающий вызов функции) — без auto(x) пришлось бы явно писать std::decay_t<T>.A template function taking a forwarding parameter that needs, in one place, an independent copy of the value (e.g. to safely store it in a container that outlives the function call) — without auto(x) this would require explicitly writing std::decay_t<T>.
template<class T>
void store(T&& value) {
history_.push_back(auto(value)); // decay-копия, не зависящая от того, T& или T&& пришло// a decay-copy, independent of whether T& or T&& was passed
}
При переборе контейнера по ссылке иногда нужен независимый снимок значения текущей итерации для передачи в асинхронную задачу — auto(x) явно отделяет "то, что переживёт итерацию" от "ссылки, валидной только в текущей итерации цикла".When iterating a container by reference, an independent snapshot of the current iteration's value is sometimes needed to hand off to an async task — auto(x) explicitly separates "what will outlive the iteration" from "a reference valid only for the current loop iteration".
for (auto& task : tasks) {
pool.submit([copy = auto(task)]() { copy.run(); }); // copy переживёт итерацию цикла// copy outlives the loop iteration
}
Ровно как с классической ловушкой std::vector<int> v(5) (вектор из 5 нулей) против std::vector<int> v{5} (вектор из одного элемента — числа 5), разница между auto(x) и auto{x} может быть значимой для типов с конструктором от std::initializer_list. Если тип, который выведет auto, имеет такой конструктор, auto{x} предпочтёт его там, где auto(x) выбрал бы обычный конструктор — стоит по умолчанию использовать круглые скобки auto(x), если нет осознанной причины требовать именно списочную инициализацию.Exactly as with the classic std::vector<int> v(5) (a vector of five zeros) versus std::vector<int> v{5} (a vector of one element — the number 5) trap, the difference between auto(x) and auto{x} can matter for types with an std::initializer_list constructor. If the type auto deduces has such a constructor, auto{x} will prefer it where auto(x) would have picked an ordinary constructor — it's worth defaulting to the parenthesized auto(x) unless there's a deliberate reason to require list initialization specifically.
auto(x) внешне похож на приведение типа, но принципиально отличается: static_cast<T>(x) требует явно назвать целевой тип T и может выполнять произвольное приведение (сужающее, пользовательское, между несвязанными типами), тогда как auto(x) не принимает никакого типа вообще — тип всегда выводится по тем же правилам, что и для auto-переменной, и единственная цель конструкции — decay-copy, а не приведение к произвольному другому типу.auto(x) superficially resembles a type cast, but is fundamentally different: static_cast<T>(x) requires explicitly naming a target type T and can perform an arbitrary conversion (narrowing, user-defined, between unrelated types), whereas auto(x) takes no type at all — the type is always deduced by the same rules as for an auto variable, and the construct's sole purpose is a decay-copy, not conversion to an arbitrary different type.
Все тонкости вывода типа для обычной переменной auto x = expr; (снятие ссылок, снятие верхнеуровневых cv-квалификаторов, распад массивов и функций в указатели, поведение с auto*/auto&, которые здесь неприменимы, так как auto(x) — всегда объект по значению) переносятся на auto(x) без изменений — если вывод типа для auto в конкретном случае не очевиден, стоит освежить в памяти механику, описанную в cpp11/auto.html, прежде чем полагаться на auto(x) в неочевидном месте.All the subtleties of type deduction for an ordinary auto x = expr; variable (stripping references, stripping top-level cv-qualifiers, decaying arrays and functions to pointers, the behavior with auto*/auto&, which don't apply here since auto(x) is always an object by value) carry over to auto(x) unchanged — if type deduction for auto isn't obvious in a given case, it's worth refreshing the mechanics described in cpp11/auto.html before relying on auto(x) in a non-obvious spot.