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.