auto type deduction rules

C++
Author

Quasar

Published

December 21, 2025

Introduction

This weekend, I tried to solve a fun puzzle - the C++ auto type deduction gauntlet and thought of creating a quick cheatsheet on auto type deduction rules. I encourage you to give it a shot.

auto is a placeholder type that gets replaced typically by deduction from an initializer.

Whenever you have:

auto [some_modifiers] x = expression;

you have three general cases:

  • auto x : The object being assigned is allowed to decay. We strip the RHS expression of all CV-qualifiers such as const and reference modifiers & to deduce the by-value type of x.
  • auto& x or auto&& x : It preserves references and CV-qualifiers.
  • const auto& x : We specify the qualifiers, auto simply deduces the base type.

There is special treatment for expressions that are functions or arrays:

  • When initializing a reference(auto&), array/function types are preserved.
  • Otherwise, it decays to a pointer before type deduction.

Exercises to flex your understanding of auto deduction rules

Let’s determine for each of the problems:

  • The deduced type
  • The value category
  • What CV qualifiers are applicable

As the author of the puzzles states, let’s note that these are meant to test your understanding and some examples won’t compile.

Basics

auto v = 5;

This is fairly straight-forward. The deduced type is int. The value category of v is lvalue.

auto v = 0.1;

Floating-point values default to the larger double, rather than float. The value category of v is lvalue.

int x;
auto v = x;

The deduced type is int and the value category of v is lvalue.

auto i{0uz};

The u suffix is used for unsigned integer literals and the z suffix is used for the signed version of std::size_t. The suffix uz deduces to std::size_t.

The type size_t is an implementation-defined unsigned integer type that is large enough to contain the size in bytes of any object.

auto v=5, w=0.1

I didn’t get this puzzle. But, this will fail to compile. All types in an expression defined with auto have be the same.

int x;
auto v = &x;

& is the address-of operator. So, the type of v is deduced as pointer-to-int int*. The value category of v is lvalue.

int x[5];
auto v = x;

x is an array of \(5\) ints, its type is int [5]. C-style arrays decay to a pointer. So, the type of v is int*. v is an lvalue.

auto v = nullptr;

nullptr is a value of type std::nullptr_t.

auto v = {1, 2, 3};

\(1\), \(2\) and \(3\) are ints. So, the type of v is deduced as std::initializer_list<int>. If the curly-braced initializer list were {1, 2.5, 4}, type deduction would fail, because we require all types in an initializer list to be the same.

auto v{1, 2, 3};

Curly braced direct initialization only works with a single scalar value e.g. auto v{1}.

auto x = {17};

The type of x is deduced as std::initializer_list<int>.

To summarize:

  • auto + copy list initialization : If all elements are of type T, auto is deduced as std::initializer_list<T>.
  • auto + direct list initialization :
    • \(1\) element in braces \(\implies\) auto deduces the type of element by-value.
    • \(>1\) element \(\implies\) error(ill-formed).
int foo(int x){
    return x;
}

auto v = foo;

v is deduced as a function pointer, int (*) int. According to the C++ standard, a function is an object that occupies memory storage and has a lifetime. Hence, foo is an lvalue expression.

Intermediate

We now explore how references and CV-qualifiers are handled.

volatile const int x = 1;
auto v = x;

We strip all modifiers off x, so that results in an int. Thus, auto is deduced as int. auto always drops top-level CV qualifiers.

volatile const int x = 1;
auto v = &x;

I didn’t quite get this one. It turns out that CV qualifiers applied to pointed-to. So, type of x is deduced as volatile const int*. The next code snip also shows the same:

#include <cassert>
#include <type_traits>

int main(){
    const int& x{42};
    auto& y = x;
    static_assert(std::is_same_v<decltype(y), const int&>);
    return 0;
}

Compiler Explorer

int x;
int& y = x;
auto v = y;

y is an lvalue reference to x. The type of y decays to int.

int x;
auto& v = x;

The type of v is deduced as int&. v is an lvalue reference.

int x[5];
auto& v = x;

auto is deduced as int [5]. The type of v is a reference to an array of \(5\) ints.

int foo(const int x) {
    return x;
}
auto v = foo;

Functions are lvalues and the function name is a pointer to function object in memory. CV-qualifiers on parameters are thrown away during function resolution. Hence, auto is deduced as int (*)(int).

Advanced

int x;
auto&& v = x;

auto&& v is a forwarding reference. x is an lvalue and lvalues bind to lvalue references. Here, we get an lvalue reference.

auto x = [] () -> int { 
    return 1;
};
auto&& v = x();

The return value of the function call is a prvalue. It will bind to an rvalue reference. So, the type of v is deduced as int&&.

int x;
auto y = [&] () -> int& { 
    return x;
};
auto&& v = y();

The result of the function call y() is an lvalue, so the type of v is deduced to be int&.

struct Foo {};
auto&& v = Foo{};

The call to the constructor Foo{} creates a temporary Foo instance - a prvalue. Hence, the type of v is deduced as Foo&&.

void f(auto& param);

int x{22};
const int cx = x;
const int& rx = x;

f(x);   // [1]
f(cx);  // [2]
f(rx);  // [3]

auto& preserves references and CV-qualifiers. In the function call f(x), the type of param is deduced as int&. In the function call f(cx), the type of param is deduced as const int&. In the function call f(rx), the type of param is deduced as const int&.

Solving these auto type deduction puzzles prompted me to go watch Scott Meyer’s talk on type deduction from CppCon 2014. At 25:10, he says:

It is important to distinguish between an expression that is const, and an expression that contains const.

The most common place this issue occurs, is when we are dealing with pointers. Observe the following code snip:

int* const cpi;
auto p1 = cpi;

const int* pci;
auto p2 = pci;

const int* const cpci;
auto p3 = cpci;

You can have a constant pointer-to-int, in which case the expression is a constant. Or you can have a pointer-to-const, in which case you have a expression containing const. And you can have a constant pointer to const, which is a constant expression that contains a const.

When deducing auto, we are deducing by-value. The top-level const/volatile is dropped. Applying this rule, we have the following:

  • The type of p1 is deduced as int*.
  • The type of p2 is deduced as const int*.
  • The type of p3 is deduced as const int*.

THe fact that p3 is const is ignored. The fact that p3 contains const is not ignored.

int x{22};
const int* pcx = &x;
auto ppcx = &pcx;

The expression &pcx contains const. So, the type of ppcx is deduced as const int**.

References