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:
jis deduced asintsince top-levelconstis ignored.- When
autois used with reference (&) or pointer (*), theconstqualifier is preserved in the deduced type. const autoapplies a newconstto 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:
*dyields aBase&, but sincebis declared withauto(notauto&), the result is value-initialized.- This results in object slicing, where the
Derivedpart of the object is sliced off, andbbecomes a pureBaseobject. - 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 |