C++ Module 2 – Types, values and expressions
C++ auto, decltype and type aliases
What auto deduces from references and const, how decltype names the type of an expression, and how using makes aliases: every rule proved by the compiler.
What you will learn
- Use auto where the type is obvious or cannot be spelled
- Predict what auto deduces from references and const
- Write readable aliases with using
Before you start
What you’ll learn · Before you start
What you will learn
- Use auto where the type is obvious or cannot be spelled
- Predict what auto deduces from references and const
- Write readable aliases with using
Before you start
On this page
auto in a declaration tells the compiler to take the variable’s type from its initializer: auto n = 7; makes
n an int. It removes long type names, it is the only way to declare a variable whose type cannot be written, such as
a lambda, and it never leaves a variable uninitialised. But it deduces the type of the value, so it drops references and the const of
the initializer itself: auto x = some_reference; makes a copy. Write auto& or const auto& when you want a
reference. decltype(expr) gives the type of an expression exactly as declared, references and const
included. using Name = Type; creates an alias, a second name for an existing type, and also works for
templates. This lesson proves every deduction with static_assert, so the compiler itself confirms each claim, on
Apple clang 21 and GCC 16.
auto takes the type of the value
Each line below declares a variable with auto, and a static_assert beneath states its type; if any statement were
wrong, the file would not compile. std::is_same_v<A, B> is true exactly when A and B are the same type.
// What auto deduces, checked by the compiler:
// each static_assert stops the build if the type is not the one it names.
#include <print>
#include <string>
#include <type_traits>
#include <vector>
int main() {
int count = 7;
const int limit = 10;
int& ref = count;
const int& cref = limit;
const int* p = &limit;
int scores[3] = {90, 75, 60};
std::vector<std::string> names{"Asha", "Ravi"};
auto a = count; // int
auto b = limit; // int: the const of the variable itself is dropped
auto c = ref; // int: a copy, the reference is dropped
auto d = cref; // int: both dropped
auto e = p; // const int*: a pointer to const keeps that const
auto f = scores; // int*: an array decays to a pointer to its first element
auto g = "chai"; // const char*: a string literal is an array of const char
auto h = names[0]; // std::string: a copy of the element
auto i{5}; // int (since C++17)
auto j = {5}; // std::initializer_list<int>, not int
static_assert(std::is_same_v<decltype(a), int>);
static_assert(std::is_same_v<decltype(b), int>);
static_assert(std::is_same_v<decltype(c), int>);
static_assert(std::is_same_v<decltype(d), int>);
static_assert(std::is_same_v<decltype(e), const int*>);
static_assert(std::is_same_v<decltype(f), int*>);
static_assert(std::is_same_v<decltype(g), const char*>);
static_assert(std::is_same_v<decltype(h), std::string>);
static_assert(std::is_same_v<decltype(i), int>);
static_assert(std::is_same_v<decltype(j), std::initializer_list<int>>);
c = 100; // changes the copy only, not count
std::println("a={} b={} c={} d={} *e={} f[0]={}", a, b, c, d, *e, f[0]);
std::println("g={} h={} i={} j.size()={} count={}", g, h, i, j.size(), count);
} from session import run
run("""
clang++ --version | head -n 1
clang++ -std=c++23 -Wall -Wextra deduction.cpp -o deduction && ./deduction
g++-16 --version | head -n 1
g++-16 -std=c++23 -Wall -Wextra deduction.cpp -o deduction && ./deduction
""") Output
$ clang++ --version | head -n 1 Apple clang version 21.0.0 (clang-2100.3.34.2) $ clang++ -std=c++23 -Wall -Wextra deduction.cpp -o deduction && ./deduction a=7 b=10 c=100 d=10 *e=10 f[0]=90 g=chai h=Asha i=5 j.size()=1 count=7 $ g++-16 --version | head -n 1 g++-16 (Homebrew GCC 16.2.0) 16.2.0 $ g++-16 -std=c++23 -Wall -Wextra deduction.cpp -o deduction && ./deduction a=7 b=10 c=100 d=10 *e=10 f[0]=90 g=chai h=Asha i=5 j.size()=1 count=7
Recorded with Python 3.14.8 on macOS 26 arm64. To run it yourself: mise exec python@3.14.8 -- python3 deduction_session.py
The file builds on both compilers, so all ten statements hold. The rules behind them:
- The reference is dropped.
canddcome from references, but they are newintvariables holding copies: afterc = 100;,countis still 7. - Top-level
constis dropped.bis a plainintalthoughlimitisconst int:constdescribes the variablelimit, andbis a different variable. Aconstthat belongs to the type pointed to is kept, soeisconst int*: it still may not changelimitthrough the pointer. - Arrays become pointers.
fisint*, a pointer toscores[0], andg, from a string literal, isconst char*. Neither copies the array. - Braces need care. Since C++17
auto i{5};is anint, butauto j = {5};is astd::initializer_list<int>, a list with one element, which is rarely what was meant.
What auto and auto& deduce from a const reference
Text description of the diagram
The diagram starts from one initializer, const int& cref = limit;, and follows two arrows.
- The arrow labelled auto goes down through two steps. First the reference is dropped, which leaves const int. Then the top-level const is dropped, which leaves int. So auto x = cref; makes x an int, a copy of the value.
- The arrow labelled auto& goes to auto& r = cref;. With the & written out, nothing is dropped: r is a const int&, a reference to the same object that still cannot be changed through r.
auto&, const auto& and auto&&
When you want the original object rather than a copy, say so. The difference matters most in range-based for
loops:
// auto makes a copy; auto& and const auto& refer to the original.
// The loop that doubles nothing is a classic bug.
#include <print>
#include <string>
#include <vector>
int main() {
std::vector<int> prices{40, 25, 60};
for (auto p : prices) p *= 2; // p is a copy of each element: the vector does not change
std::println("after auto: {}", prices);
for (auto& p : prices) p *= 2; // p refers to each element: the vector changes
std::println("after auto&: {}", prices);
const std::vector<std::string> cities{"Bengaluru", "Chennai", "Kolkata"};
std::size_t letters = 0;
for (const auto& city : cities) letters += city.size(); // reads each string without copying it
std::println("{} letters in {} names", letters, cities.size());
int x = 1;
auto&& r1 = x; // a forwarding reference: binds to x as int&
auto&& r2 = 42; // ... and to a temporary as int&&
r1 = 5;
std::println("x = {}, r2 = {}", x, r2);
} from session import run
run("""
clang++ -std=c++23 -Wall -Wextra references.cpp -o references && ./references
g++-16 -std=c++23 -Wall -Wextra references.cpp -o references && ./references
""") Output
$ clang++ -std=c++23 -Wall -Wextra references.cpp -o references && ./references
references.cpp:10:15: warning: variable 'p' set but not used [-Wunused-but-set-variable]
10 | for (auto p : prices) p *= 2; // p is a copy of each element: the vector does not change
| ^
1 warning generated.
after auto: [40, 25, 60]
after auto&: [80, 50, 120]
23 letters in 3 names
x = 5, r2 = 42
$ g++-16 -std=c++23 -Wall -Wextra references.cpp -o references && ./references
references.cpp: In function 'int main()':
references.cpp:10:15: warning: variable 'p' set but not used [-Wunused-but-set-variable=]
10 | for (auto p : prices) p *= 2; // p is a copy of each element: the vector does not change
| ^
after auto: [40, 25, 60]
after auto&: [80, 50, 120]
23 letters in 3 names
x = 5, r2 = 42
Recorded with Python 3.14.8 on macOS 26 arm64. To run it yourself: mise exec python@3.14.8 -- python3 references_session.py
The first loop doubled copies, so the vector did not change. Both compilers warn that p is set but never used, which
is the clue here; for a copy of a class type, such as a std::string, they stay silent, so the habit of writing
references in loops matters more than the warning. auto& refers to each element, so the second loop changed them. const auto& reads each string
without copying it, which is the usual way to loop over strings or other large objects that you only read.
auto&& is a forwarding reference: it binds to anything, as int& to a named variable (r1 changed x) and as
int&& to a temporary such as 42. You will meet it in generic code; in everyday loops auto& and const auto&
say more clearly what you mean.
decltype: the declared type, exactly
decltype(e) gives the type of an expression without evaluating it, and it keeps what auto drops:
// decltype(name) is the declared type of a variable;
// decltype((name)), with extra parentheses, is a reference.
#include <print>
#include <type_traits>
#include <vector>
int main() {
int x = 0;
const int c = 1;
std::vector<int> v{1, 2, 3};
static_assert(std::is_same_v<decltype(x), int>);
// (x) is an expression that names an object
static_assert(std::is_same_v<decltype((x)), int&>);
// decltype keeps the const that auto drops
static_assert(std::is_same_v<decltype(c), const int>);
static_assert(std::is_same_v<decltype(v[0]), int&>); // what operator[] returns
static_assert(std::is_same_v<decltype(x + 1), int>); // a computed value, not a reference
auto copy = v[0]; // int
decltype(auto) same = v[0]; // int&: the exact type of the initializer
same = 10;
copy = 20;
std::println("v = {}, copy = {}", v, copy);
} from session import run
run("""
clang++ -std=c++23 -Wall -Wextra decltype_rules.cpp -o decltype_rules && ./decltype_rules
g++-16 -std=c++23 -Wall -Wextra decltype_rules.cpp -o decltype_rules && ./decltype_rules
""") Output
$ clang++ -std=c++23 -Wall -Wextra decltype_rules.cpp -o decltype_rules && ./decltype_rules v = [10, 2, 3], copy = 20 $ g++-16 -std=c++23 -Wall -Wextra decltype_rules.cpp -o decltype_rules && ./decltype_rules v = [10, 2, 3], copy = 20
Recorded with Python 3.14.8 on macOS 26 arm64. To run it yourself: mise exec python@3.14.8 -- python3 decltype_session.py
decltype(c) is const int, the declared type of c. decltype(v[0]) is int&, because operator[] returns a
reference. The odd rule is the one with extra parentheses: decltype(x) names the variable and gives its declared
type, int, while decltype((x)) is an expression that refers to an object, an lvalue, and so gives int&.
decltype(auto) deduces like decltype instead of like auto: same became an int&, so same = 10; changed
v[0], while copy = 20; changed only the copy. It is mostly used for function return types that must keep a
reference.
Where auto hides a surprise
auto takes whatever type the initializer has, even when that is not the type you had in mind.
std::vector<bool> stores its values as single bits, so flags[i] cannot return a bool&; it returns a small proxy
object, std::vector<bool>::reference, that points into the vector. With auto, the variable is that proxy:
// std::vector<bool> packs its values into bits, so v[i] returns a small proxy object, not a bool&.
// auto then deduces the proxy type, which still points into the vector.
#include <print>
#include <type_traits>
#include <vector>
std::vector<bool> make_flags() { return std::vector<bool>(1000, false); }
int main() {
std::vector<bool> flags(1000, false);
auto first = flags[0]; // not a bool: a std::vector<bool>::reference
static_assert(!std::is_same_v<decltype(first), bool>);
first = true; // writes into the vector
std::println("flags[0] is now {}", static_cast<bool>(flags[0]));
bool copy = flags[1]; // naming the type converts the proxy to a plain bool
copy = true;
std::println("flags[1] is still {}, copy is {}", static_cast<bool>(flags[1]), copy);
auto dangling = make_flags()[0]; // a proxy into a temporary, destroyed at the end of this line
std::println("reading it: {}", static_cast<bool>(dangling)); // reads freed memory
} from session import run
run("""
clang++ -std=c++23 -Wall -Wextra -g -fsanitize=address,undefined vector_bool.cpp -o vector_bool && ./vector_bool
g++-16 -std=c++23 -Wall -Wextra -g -fsanitize=address,undefined vector_bool.cpp -o vector_bool && ./vector_bool
""") Output
$ clang++ -std=c++23 -Wall -Wextra -g -fsanitize=address,undefined vector_bool.cpp -o vector_bool && ./vector_bool
flags[0] is now true
flags[1] is still false, copy is true
=================================================================
==…==ERROR: AddressSanitizer: heap-use-after-free on address 0x…
READ of size 8 at 0x… thread T0
#1 in main vector_bool.cpp:22
0x… is located 0 bytes inside of 128-byte region [0x…,0x…)
freed by thread T0 here:
#7 in main vector_bool.cpp:21
previously allocated by thread T0 here:
#9 in make_flags() vector_bool.cpp:7
#10 in main vector_bool.cpp:21
SUMMARY: AddressSanitizer: heap-use-after-free
[56 more lines of the report left out]
[exit status 134]
$ g++-16 -std=c++23 -Wall -Wextra -g -fsanitize=address,undefined vector_bool.cpp -o vector_bool && ./vector_bool
flags[0] is now true
flags[1] is still false, copy is true
=================================================================
==…==ERROR: AddressSanitizer: heap-use-after-free on address 0x…
READ of size 8 at 0x… thread T0
#1 in main vector_bool.cpp:22
0x… is located 0 bytes inside of 128-byte region [0x…,0x…)
freed by thread T0 here:
#5 in main vector_bool.cpp:21
previously allocated by thread T0 here:
#5 in make_flags() vector_bool.cpp:7
#6 in main vector_bool.cpp:21
SUMMARY: AddressSanitizer: heap-use-after-free
[50 more lines of the report left out]
[exit status 134]
Recorded with Python 3.14.8 on macOS 26 arm64. To run it yourself: mise exec python@3.14.8 -- python3 vector_bool_session.py
first = true; wrote into the vector, while the bool copy did not. (AddressSanitizer reports run to dozens of
lines, so the sessions keep the lines that name the error and this lesson’s own files, and write the memory addresses,
which change on every run, as 0x….) The last two lines are the dangerous case:
make_flags()[0] is a proxy into a temporary vector that is destroyed at the end of the line, so reading it reads
freed memory. AddressSanitizer stops the program and names both lines, line 21 where the memory was allocated and
freed and line 22 where it was read; without the sanitizer the program might print a plausible value. Write the type
(bool b = flags[0];) when you want the value.
Number types are the other surprise. A literal or an expression has a type of its own, and auto keeps it:
// auto takes the type of a literal or an expression exactly as it is,
// which is not always the type you wanted.
#include <cstddef>
#include <print>
#include <type_traits>
#include <vector>
int main() {
auto small = 3'000; // int
auto big = 3'000'000'000; // too big for int: long here, long long on Windows
auto size = std::vector<int>(5).size(); // std::size_t, an unsigned type
static_assert(std::is_same_v<decltype(small), int>);
static_assert(std::is_same_v<decltype(big), long>);
static_assert(std::is_same_v<decltype(size), std::size_t>);
std::vector<int> v{3, 1, 4};
for (auto i = 0; i < v.size(); ++i) { // int i against an unsigned size: a mixed comparison
std::print("{} ", v[i]);
}
std::println("| sizes in bytes: {} {} {}", sizeof small, sizeof big, sizeof size);
} from session import run
run("""
clang++ -std=c++23 -Wall -Wextra integer_auto.cpp -o integer_auto && ./integer_auto
g++-16 -std=c++23 -Wall -Wextra integer_auto.cpp -o integer_auto && ./integer_auto
""") Output
$ clang++ -std=c++23 -Wall -Wextra integer_auto.cpp -o integer_auto && ./integer_auto
integer_auto.cpp:17:24: warning: comparison of integers of different signs: 'int' and 'size_type' (aka 'unsigned long') [-Wsign-compare]
17 | for (auto i = 0; i < v.size(); ++i) { // int i against an unsigned size: a mixed comparison
| ~ ^ ~~~~~~~~
1 warning generated.
3 1 4 | sizes in bytes: 4 8 8
$ g++-16 -std=c++23 -Wall -Wextra integer_auto.cpp -o integer_auto && ./integer_auto
integer_auto.cpp: In function 'int main()':
integer_auto.cpp:17:24: warning: comparison of integer expressions of different signedness: 'int' and 'std::vector<int>::size_type' {aka 'long unsigned int'} [-Wsign-compare]
17 | for (auto i = 0; i < v.size(); ++i) { // int i against an unsigned size: a mixed comparison
| ~~^~~~~~~~~~
3 1 4 | sizes in bytes: 4 8 8
Recorded with Python 3.14.8 on macOS 26 arm64. To run it yourself: mise exec python@3.14.8 -- python3 integer_auto_session.py
3'000'000'000 does not fit in a 32-bit int, so the literal is a long, 8 bytes on 64-bit macOS and Linux. On
Windows long has 4 bytes, so the same literal is a long long there. size() returns std::size_t, an unsigned
type, while auto i = 0 makes an int, and the comparison of the two in the loop condition gets a
-Wsign-compare warning from both compilers. In such a comparison the int is converted to the unsigned type, so a
negative value would compare as a huge number. Declare the loop variable as std::size_t, or loop over the elements
with for (int x : v).
Type aliases with using
using Name = Type; gives an existing type a second name. It reads left to right, like an assignment, and unlike the
older typedef it can also be a template:
// using gives an existing type a second name. It does not create a new type,
// so mixing up two aliases of the same type still compiles.
#include <cstdint>
#include <map>
#include <print>
#include <string>
#include <type_traits>
#include <vector>
using Score = std::int32_t;
// much shorter to write than the type it names
using Marks = std::map<std::string, std::vector<Score>>;
template <typename T>
// an alias template: typedef cannot do this
using Grid = std::vector<std::vector<T>>;
typedef std::int32_t OldScore; // the older way to write the same alias
using Metres = double;
using Seconds = double;
int main() {
static_assert(std::is_same_v<Score, std::int32_t>);
static_assert(std::is_same_v<Score, OldScore>);
Marks marks{{"Asha", {78, 91}}, {"Ravi", {64}}};
Grid<char> board(2, std::vector<char>(3, '.'));
board[1][2] = 'X';
const Metres distance = 1500.0;
const Seconds time = 300.0;
const double nonsense = distance + time; // compiles: both are just double
std::println("{} students, board {}x{}, row 1 = {}, distance + time = {}", marks.size(), board.size(),
board[0].size(), std::string(board[1].begin(), board[1].end()), nonsense);
} from session import run
run("""
clang++ -std=c++23 -Wall -Wextra aliases.cpp -o aliases && ./aliases
g++-16 -std=c++23 -Wall -Wextra aliases.cpp -o aliases && ./aliases
""") Output
$ clang++ -std=c++23 -Wall -Wextra aliases.cpp -o aliases && ./aliases 2 students, board 2x3, row 1 = ..X, distance + time = 1800 $ g++-16 -std=c++23 -Wall -Wextra aliases.cpp -o aliases && ./aliases 2 students, board 2x3, row 1 = ..X, distance + time = 1800
Recorded with Python 3.14.8 on macOS 26 arm64. To run it yourself: mise exec python@3.14.8 -- python3 aliases_session.py
Marks saves writing std::map<std::string, std::vector<std::int32_t>> everywhere, and Grid<char> is a
std::vector<std::vector<char>>. The C++ Core Guidelines recommend using over typedef (rule T.43) for these
reasons.
An alias is not a new type. Score and std::int32_t are the same type, which the first static_assert proves,
and so are Metres and Seconds: adding a distance to a time compiled and printed 1800. When two kinds of value must
never be mixed, make them different types: small structs (a later module) or an
enum class.
When to use auto
- Use it when the type is already visible in the initializer (
auto p = std::make_unique<Order>();,auto n = static_cast<int>(x);), when the type is long and adds nothing (iterators, map entries), and when it cannot be written at all, as for lambdas. The Core Guidelines give the same advice in rule ES.11. - Add
&orconst auto&whenever you mean the original object, especially in loops. - Write the type when the exact type is the point: integer widths,
boolfromstd::vector<bool>, a value that must not narrow.
Exercise: predict ten types
For each declaration in the file, replace void with the type that auto or decltype deduces on that line. The
static_assert lines check every answer while the file compiles, so the exercise is solved when it builds. Write
your answers first; a failing check then shows the real type, so you can learn from each one.
// Exercise: replace each void with the type that auto or decltype deduces on the same line.
// The static_asserts at the end check every answer: it is solved when the file builds.
#include <string>
#include <type_traits>
#include <vector>
int main() {
const int limit = 10;
int count = 3;
int& ref = count;
const std::string name = "Meera";
std::vector<int> v{1, 2, 3};
std::vector<bool> bits(8);
auto t1 = limit; using answer1 = void;
auto& t2 = limit; using answer2 = void;
auto t3 = ref; using answer3 = void;
decltype(ref) t4 = count; using answer4 = void;
decltype((count)) t5 = count; using answer5 = void;
auto t6 = name.size(); using answer6 = void;
auto t7 = v[0] * 2.0; using answer7 = void;
const auto& t8 = name; using answer8 = void;
auto t9 = 'a' + 1; using answer9 = void;
auto t10 = bits[0]; using answer10 = void;
static_assert(std::is_same_v<decltype(t1), answer1>, "t1");
static_assert(std::is_same_v<decltype(t2), answer2>, "t2");
static_assert(std::is_same_v<decltype(t3), answer3>, "t3");
static_assert(std::is_same_v<decltype(t4), answer4>, "t4");
static_assert(std::is_same_v<decltype(t5), answer5>, "t5");
static_assert(std::is_same_v<decltype(t6), answer6>, "t6");
static_assert(std::is_same_v<decltype(t7), answer7>, "t7");
static_assert(std::is_same_v<decltype(t8), answer8>, "t8");
static_assert(std::is_same_v<decltype(t9), answer9>, "t9");
static_assert(std::is_same_v<decltype(t10), answer10>, "t10");
(void)t1; (void)t2; (void)t3; (void)t4; (void)t5; (void)t6; (void)t7; (void)t8; (void)t9; (void)t10;
} As given, all ten checks fail. The session counts them without showing the compiler’s messages, which contain the answers:
from session import run
run("""
# How many of the ten checks fail? grep -c counts the lines of the compiler's output that report one.
clang++ -std=c++23 -fsyntax-only predict_types.cpp 2>&1 | grep -c 'static assertion failed'
g++-16 -std=c++23 -fsyntax-only predict_types.cpp 2>&1 | grep -c 'static assertion failed'
""") Output
# How many of the ten checks fail? grep -c counts the lines of the compiler's output that report one. $ clang++ -std=c++23 -fsyntax-only predict_types.cpp 2>&1 | grep -c 'static assertion failed' 10 $ g++-16 -std=c++23 -fsyntax-only predict_types.cpp 2>&1 | grep -c 'static assertion failed' 10
Recorded with Python 3.14.8 on macOS 26 arm64. To run it yourself: mise exec python@3.14.8 -- python3 predict_types_session.py
First hint: start from the three rules of auto (references dropped, top-level const dropped, arrays and functions
become pointers), remember that decltype keeps references, and that 'a' + 1 is computed in the type that char
is promoted to.
Self-check
Check yourself
5 questions about this lesson. Every answer and why it is right is on the page, behind “Show the answer”. Your score stays in this browser.
Interview questions
What does auto deduce for auto x = r; when r is a const int&? It deduces int. auto deduces the type of
the value, so it drops the reference and then the top-level const, and x is a separate, modifiable copy. To keep
the reference write auto& x = r;, which deduces const int& because the const now belongs to the object referred
to; const auto& x = r; says the same thing more explicitly. decltype(r) x = r; would also give const int&.
Key takeaways
autodeduces the type of the initializer’s value: references and top-levelconstare dropped, arrays become pointers.auto&andconst auto&keep the original object; use them in loops over elements you change or only read.auto&&binds to anything and is mostly for generic code.decltype(x)is the declared type,decltype((x))an lvalue reference, anddecltype(auto)deduces withdecltyperules.autokeeps proxy types (std::vector<bool>::reference) and literal types (longorlong long): write the type when it matters.using Name = Type;makes an alias, also for templates; an alias is not a new type.
References
- Working Draft, Programming Languages: C++ (N4950), 9.2.9.6 Placeholder type specifiers, 9.2.9.5 Decltype specifiers, 9.2.4 The typedef specifier (ISO/IEC JTC1/SC22/WG21)
- N3922: New Rules for auto deduction from braced-init-list (ISO/IEC JTC1/SC22/WG21)
- C++ Core Guidelines: ES.11 Use auto to avoid redundant repetition of type names (Standard C++ Foundation)
- C++ Core Guidelines: T.43 Prefer using over typedef for defining aliases (Standard C++ Foundation)
- Data Type Ranges (Microsoft C++) (Microsoft)
- AddressSanitizer (LLVM Project)
Report a problem with this lesson
Kept only in this browser. Your Learn progress