How C++ Auto Return Type Deduction Handles References
In C++, function return type deduction using plain auto
follows the rules of template argument deduction, which means it strips
references and top-level const or volatile
qualifiers by default. Consequently, returning an expression that
evaluates to an lvalue reference with a bare auto return
type produces a copy of the object rather than preserving the reference.
To correctly deduce and propagate reference modifiers, C++ requires
either explicitly annotating auto with reference qualifiers
(such as auto&) or using decltype(auto) to
preserve the exact value category and qualifiers of the returned
expression.
Plain auto Discards
References
When a function uses auto without qualifiers for its
return type, the compiler deduces the type as if passing the returned
expression to a template function by value:
template <typename T>
void deduce(T param);Because of these rules, any reference modifier attached to the return expression is decayed.
int global_val = 10;
int& get_ref() {
return global_val;
}
auto get_val_auto() {
return get_ref(); // Deduces int, NOT int&
}In this case, calling get_val_auto() creates a copy of
global_val. Mutating the result of
get_val_auto() will not modify global_val.
Explicit
Reference Modifiers: auto& and
const auto&
If a function must return a reference, you can explicitly add the
reference modifier to auto. This forces the compiler to
deduce the underlying type while keeping the reference intact:
auto& get_ref_auto() {
return get_ref(); // Deduces int&
}
const auto& get_const_ref_auto() {
return get_ref(); // Deduces const int&
}While auto& solves the issue for lvalues, it creates
a compilation error if the return expression is an rvalue (such as a
temporary), because a non-const lvalue reference cannot bind to an
rvalue:
auto& fail_example() {
return 42; // Error: cannot bind non-const lvalue reference to an rvalue
}Preserving References
with decltype(auto)
Introduced in C++14 alongside standard return type deduction,
decltype(auto) solves the limitation of plain
auto. Instead of applying template deduction rules, it
applies decltype rules to the return expression, preserving
references and const modifiers exactly as they appear.
decltype(auto) forward_ref() {
return get_ref(); // Deduces int& because get_ref() returns int&
}
decltype(auto) forward_value() {
return 42; // Deduces int because 42 is a prvalue
}The Parenthesis
Pitfall with decltype(auto)
Because decltype(auto) follows decltype
rules, the presence of parentheses alters how references are deduced for
lvalues:
- Returning an unparenthesized identifier returns the declared type of the entity.
- Returning a parenthesized expression that evaluates to an lvalue
deduces an lvalue reference (
T&).
int x = 5;
decltype(auto) return_var() {
return x; // Deduces int
}
decltype(auto) return_expr() {
return (x); // Deduces int& because (x) is an lvalue expression
}Care must be taken when returning local variables with parentheses
inside a decltype(auto) function, as returning
(local_var) deduces a reference to a local variable that
will dangle upon function exit.