Type Deduction Rules in C++
The auto
keyword instructs the compiler to deduce the type of a variable based on its initializer.
However, deduction follows a set of specific rules, especially regarding references, const
qualifiers, value categories, and initializer forms.
The following summarizes the key rules with examples and explanations.
Rule 1: Top-Level CV Qualifiers Are Discarded During Value Initialization
When auto
is used to declare a variable and the initializer is a value (i.e. not a reference or pointer), any top-level const
or volatile
qualifiers in the initializer's type are ignored.
const int i = 5;
auto j = i; // deduced as int, not const int
auto& m = i; // deduced as const int&, reference preserves cv-qualifier
auto* p = &i; // deduced as const int*, pointer type retains cv-qualifier
const auto n = j; // deduced as const int
Explanation:
j
is deduced asint
since top-levelconst
is ignored.- When
auto
is used with reference (&
) or pointer (*
), theconst
qualifier is preserved in the deduced type. const auto
applies a newconst
to the deduced type ofauto
.
Rule 2: Reference Qualifiers in Initializers Are Ignored in Value Declarations
When a variable is initialized using a reference, but the declaration uses auto
without a reference, the reference in the initializer is not preserved.
int i = 5;
int& ref = i;
auto m = ref; // deduced as int, not int&
Explanation:
The type of m
is int
because the reference in ref
is discarded during value deduction.
Rule 3: Universal References Deduce Lvalue/Rvalue Appropriately
When auto&&
is used (also known as a universal or forwarding reference), the deduced type depends on the value category of the initializer:
int i = 5;
auto&& x = i; // deduced as int&, because i is an lvalue
auto&& y = 10; // deduced as int&&, because 10 is an rvalue
Explanation:
This behavior uses the reference collapsing rules. Lvalues result in T&
, and rvalues result in T&&
.
Rule 4: Array and Function Types Decay into Pointers
When auto
is used to deduce the type of an array or function, the type is deduced as a pointer.
int arr[5];
auto a = arr; // deduced as int*
int sum(int, int);
auto b = sum; // deduced as int (*)(int, int)
Explanation:
Array names decay to pointers to the first element, and function names decay to function pointers.
Rule 5: Deduction with List Initialization
C++17 introduces more precise rules for auto
with list-initialization. The deduction behavior differs between brace-init and brace-init with =
.
Case 1: Direct List Initialization (auto x{...}
)
auto x1{1}; // deduced as int
auto x2{1, 2}; // error: more than one element
- If a single element is used, the type is deduced from that element.
- Multiple elements are not permitted—this results in a compilation error.
Case 2: Copy List Initialization (auto x = {...}
)
auto y1 = {1}; // deduced as std::initializer_list<int>
auto y2 = {1, 2}; // deduced as std::initializer_list<int>
auto y3 = {1, 2.0}; // error: conflicting types, cannot deduce common T
- If multiple elements of the same type are used, the type is deduced as
std::initializer_list<T>
. - If the types differ, deduction fails due to type mismatch.
Pitfall: Object Slicing with auto
class Base {
public:
virtual void f() { std::cout << "Base::f()" << std::endl; }
};
class Derived : public Base {
public:
void f() override { std::cout << "Derived::f()" << std::endl; }
};
Base* d = new Derived();
auto b = *d; // deduced as Base (value)
b.f(); // calls Base::f() due to object slicing
Explanation:
*d
yields aBase&
, but sinceb
is declared withauto
(notauto&
), the result is value-initialized.- This results in object slicing, where the
Derived
part of the object is sliced off, andb
becomes a pureBase
object. - As a result, the virtual function call resolves to
Base::f()
.
To preserve polymorphism:
auto& b = *d; // deduced as Base&
b.f(); // correctly calls Derived::f()
Summary Table
Scenario | Deduction Result |
---|---|
auto j = const int | int (cv removed) |
auto& j = const int | const int& (cv preserved) |
auto m = ref where ref is int& | int |
auto&& m = lvalue | T& |
auto&& m = rvalue | T&& |
auto m = array | pointer to element type |
auto m = function | function pointer |
auto x = {1, 2} | std::initializer_list<int> |
auto x{1, 2} | error |
auto x = {1, 2.0} | error |