лямбды и вывод типовlambdas and type deduction
Вывод типа с точным сохранением ссылочности и cv-квалификаторов — там, где обычный auto отбрасывает их намеренно, применяя правила template argument deduction.Type deduction that preserves reference-ness and cv-qualifiers exactly — where plain auto intentionally strips them by applying template argument deduction rules.
decltype(auto) — псевдоним типа, введённый в C++14, который выводит тип по правилам decltype, применённым к тому выражению, которым переменная или возвращаемое значение инициализируются. В отличие от auto, который всегда выводит тип «по значению» (отбрасывая ссылки и верхнеуровневые cv-квалификаторы, как и вывод шаблонного параметра по значению), decltype(auto) сохраняет ссылочность и константность в точности такими, какие они у исходного выражения.decltype(auto) is a type placeholder introduced in C++14 that deduces a type using decltype rules applied to the expression that initializes the variable or return value. Unlike auto, which always deduces a type "by value" (stripping references and top-level cv-qualifiers, the same way template argument deduction by value does), decltype(auto) preserves reference-ness and constness exactly as they appear on the source expression.
int& get_ref(int& x) { return x; }
auto wrapper(int& x) {
return get_ref(x); // возвращаемый тип: int, не int&// return type: int, not int&
}
// wrapper возвращает КОПИЮ — ссылочность потеряна// wrapper returns a COPY — reference-ness is lost
int& get_ref(int& x) { return x; }
decltype(auto) wrapper(int& x) {
return get_ref(x); // возвращаемый тип: int&// return type: int&
}
// wrapper действительно возвращает ссылку на x// wrapper genuinely returns a reference to x
Проблема была в том, что auto в принципе не умеет сохранять ссылочность — это следствие правил, по которым он выводится, а не недосмотр:The problem was that auto is simply unable to preserve reference-ness — a consequence of the rules by which it's deduced, not an oversight:
auto x = expr; выводит тип x так же, как компилятор выводил бы T в template<class T> void f(T x) — верхнеуровневые const, volatile и ссылка всегда отбрасываются. Это осознанное поведение, полезное в большинстве случаев, но оно делает auto непригодным там, где нужно буквально повторить тип исходного выражения.auto x = expr; deduces the type of x the same way the compiler would deduce T in template<class T> void f(T x) — top-level const, volatile, and reference are always stripped. This is deliberate behavior, useful in most cases, but it makes auto unfit for situations where you need to literally reproduce the type of the source expression.
Функция-обёртка, которая должна прозрачно возвращать то же самое, что и вызываемая ею функция (включая ссылку, если та вернула ссылку) — до C++14 требовала -> decltype(expr) явно в trailing return type, что работало, но было многословно и требовало дублировать выражение вызова дважды: один раз для вычисления типа, второй раз в теле.A wrapper function that has to transparently return whatever the function it calls returns (including a reference, if that function returned one) required an explicit -> decltype(expr) trailing return type before C++14 — which worked but was verbose and forced duplicating the call expression twice: once to compute the type, once in the body.
decltype(expr) как самостоятельный оператор появился в C++11 именно для точного сохранения типа выражения — но использовать его вместо auto для переменных или возврата требовало вручную выписывать выражение целиком. decltype(auto) — это способ сказать «примени decltype к тому выражению, которым я инициализирую», не повторяя это выражение в объявлении типа.decltype(expr) as a standalone operator appeared in C++11 precisely to preserve an expression's type exactly — but using it instead of auto for variables or return values meant writing out the whole expression by hand. decltype(auto) is a way of saying "apply decltype to whatever expression I'm initializing with," without repeating that expression in the type declaration.
Когда компилятор видит decltype(auto), он не применяет обычные правила вывода auto — вместо этого он берёт инициализирующее выражение (или, для функции, выражение в return) и применяет к нему ровно те же правила, что применил бы оператор decltype(expr): если expr — это имя переменной, тип — это объявленный тип переменной; если expr — произвольное выражение, тип определяется его категорией значения (lvalue даёт ссылочный тип, xvalue — rvalue-ссылку, prvalue — тип по значению).When the compiler sees decltype(auto), it does not apply the ordinary auto deduction rules — instead it takes the initializing expression (or, for a function, the expression in return) and applies to it exactly the rules the decltype(expr) operator would: if expr is a variable's name, the type is that variable's declared type; if expr is an arbitrary expression, the type is determined by its value category (an lvalue yields a reference type, an xvalue an rvalue reference, a prvalue a by-value type).
int x = 42;
int& rx = x;
auto a1 = x; // int — auto всегда "по значению"// int — auto is always "by value"
decltype(auto) a2 = x; // int — x как имя переменной, тип = int// int — x as a variable name, type = int
decltype(auto) a3 = (x); // int& — (x) уже не "просто имя", а lvalue-выражение!// int& — (x) is no longer "just a name" but an lvalue expression!
decltype(auto) get() {
return rx; // имя переменной rx имеет тип int& -> возврат int&// the name rx has type int& -> return type int&
}
Последняя строка примера — классическая ловушка самого decltype, унаследованная decltype(auto): лишние скобки вокруг имени переменной превращают его в выражение, и категория значения меняется с «имя объекта» на «lvalue-выражение», что при decltype даёт ссылочный тип вместо типа самой переменной. Для generic-обёрток типичный паттерн — decltype(auto) call(F&& f, Args&&... args) { return f(std::forward<Args>(args)...); }: если f вернёт T&, обёртка вернёт T&; если вернёт T по значению — обёртка вернёт T по значению. Ни auto, ни ручной trailing return type без повторения выражения так не сработают.The last line of the example is the classic decltype pitfall, inherited by decltype(auto): extra parentheses around a variable name turn it into an expression, and the value category changes from "object name" to "lvalue expression," which under decltype yields a reference type instead of the variable's own type. A typical pattern for generic wrappers is decltype(auto) call(F&& f, Args&&... args) { return f(std::forward<Args>(args)...); }: if f returns T&, the wrapper returns T&; if it returns T by value, the wrapper returns T by value. Neither auto nor a hand-written trailing return type without repeating the expression can pull this off.
decltype(auto) f() { int x = 1; return x; } возвращает int, а decltype(auto) f() { int x = 1; return (x); } возвращает int& — ссылку на локальную переменную, которая умирает при выходе из функции. Это undefined behavior, которое компилятор не обязан диагностировать, и разница — ровно одна пара скобок.decltype(auto) f() { int x = 1; return x; } returns int, while decltype(auto) f() { int x = 1; return (x); } returns int& — a reference to a local variable that dies when the function exits. This is undefined behavior that the compiler isn't required to diagnose, and the difference is exactly one pair of parentheses.
Частая ошибка — думать, что decltype(auto) это auto с опцией «сохранять ссылки, если получится». На деле это полностью самостоятельный механизм вывода, использующий правила decltype, а не template argument deduction — путать их означает не понимать, откуда вообще берётся разница в поведении.A common mistake is thinking of decltype(auto) as auto with an option to "preserve references when possible." In reality it's a fully independent deduction mechanism using decltype rules rather than template argument deduction — conflating the two means not understanding where the behavioral difference actually comes from.
Как и auto, decltype(auto) x; без инициализатора не компилируется — компилятору не из чего выводить тип. Это не специфичная для decltype(auto) проблема, но при отладке ошибки вывода стоит вначале проверить именно это, а не искать проблему в правилах категории значений.Just like auto, decltype(auto) x; without an initializer doesn't compile — the compiler has nothing to deduce the type from. This isn't specific to decltype(auto), but when debugging a deduction error it's worth checking this first before chasing value-category rules.