Keyboard shortcuts

Press or to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

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 as int since top-level const is ignored.
  • When auto is used with reference (&) or pointer (*), the const qualifier is preserved in the deduced type.
  • const auto applies a new const to the deduced type of auto.

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 a Base&, but since b is declared with auto (not auto&), the result is value-initialized.
  • This results in object slicing, where the Derived part of the object is sliced off, and b becomes a pure Base 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

ScenarioDeduction Result
auto j = const intint (cv removed)
auto& j = const intconst int& (cv preserved)
auto m = ref where ref is int&int
auto&& m = lvalueT&
auto&& m = rvalueT&&
auto m = arraypointer to element type
auto m = functionfunction pointer
auto x = {1, 2}std::initializer_list<int>
auto x{1, 2}error
auto x = {1, 2.0}error