auto, decltype and structured bindings

auto asks the compiler to deduce a variable's type from its initialiser. The deduction follows the rules of template argument deduction, which is why it is predictable once you know them, and why the surprises in this lesson are all about the qualifiers auto quietly removes.

The lesson also covers the two companions of auto: decltype, which names the type of an expression without evaluating it, and structured bindings, which unpack a struct or a pair into named pieces. All three exist to keep types out of the source where the type is obvious or unnameable.

auto takes the value type

auto whole = 7; gives an int because 7 is an int, auto real = 2.5; gives a double, and auto letter = 'x'; gives a char. The deduced type is the type of the initialiser with references and top-level const removed.

Removing the qualifiers is the important part: auto x = some_const_int; makes an int, not a const int, because the new variable is a copy and the copy can be changed. If you need the const, write const auto.

The program prints sizeof for the three variables and their values. sizeof confirms the deduction: four bytes for int, eight for double, one for char on this machine.

#include <iostream> int main() { auto whole = 7; auto real = 2.5; auto letter = 'x'; std::cout << sizeof(whole) << ' ' << sizeof(real) << ' ' << sizeof(letter) << '\n'; std::cout << whole << ' ' << real << ' ' << letter << '\n'; }

g++ -std=c++23 -Wall -Wextra auto.cpp -o auto_demo
./auto_demo

4 8 1
7 2.5 x

auto& keeps the reference

A range-for loop with auto value copies every element, so a change to value is lost when the iteration ends. auto& value binds a reference and the change reaches the container.

The program runs two loops over the same vector: the first doubles copies and changes nothing, the second doubles the elements themselves. The printed values are doubled once, not four times.

For a container of large objects, const auto& is the read-only form that avoids both the copy and the accidental write. It is the loop variable most read-only loops should use.

#include <iostream> #include <vector> int main() { std::vector<int> values{1, 2, 3}; for (auto value : values) { value *= 2; } for (auto& value : values) { value *= 2; } for (auto value : values) { std::cout << value << ' '; } std::cout << '\n'; }

g++ -std=c++23 -Wall -Wextra auto_ref.cpp -o auto_ref
./auto_ref

2 4 6

const auto& for reading

const auto& in a range-for names each element without copying it and without allowing a write. The compiler deduces the element type, adds the const and binds a reference, which is exactly the parameter type a read-only function would take.

The program walks a vector of std::string and prints each length. The strings themselves are not copied; only the view of each element is created.

This is the idiom to remember for loops over containers of strings, vectors or structs: for (const auto& item : items). Keep plain auto for int and double, where the copy is cheaper than the indirection.

#include <iostream> #include <string> #include <vector> int main() { std::vector<std::string> words{"alpha", "beta"}; for (const auto& word : words) { std::cout << word.size() << ' '; } std::cout << '\n'; }

g++ -std=c++23 -Wall -Wextra const_auto.cpp -o const_auto
./const_auto

5 4

decltype names a type

decltype(expression) is the type the expression would have, without evaluating it. decltype(count) is int, and decltype(1 + 2.5) is double, because the usual arithmetic conversions apply to the expression.

The main use is a variable whose type must match an expression the author does not want to spell, such as an iterator or a type from a template. Here it shows the deduction directly: copy is an int and total is a double.

One detail is worth knowing early: decltype((name)) with extra parentheses is a reference type, while decltype(name) is the type of the name. The difference matters in generic code, which is where decltype usually appears.

#include <iostream> int main() { int count = 3; decltype(count) copy = count; decltype(1 + 2.5) total = 1 + 2.5; copy += 1; std::cout << count << ' ' << copy << ' ' << total << '\n'; std::cout << sizeof(decltype('a')) << '\n'; }

g++ -std=c++23 -Wall -Wextra decltype.cpp -o decltype_demo
./decltype_demo

3 4 3.5
1

Structured bindings

A structured binding unpacks a struct, a pair or an array into named variables in one declaration. auto [x, y] = p; creates two ints from the members of p, and the names are the ones the code needs, not the member names.

The program unpacks a point and a std::pair. The binding copies the values here; auto& [x, y] = p; would bind references and allow writes through them, which is the same const-and-reference choice as above.

Structured bindings become much more useful with the containers of part 5, where a loop over a map yields a pair of key and value that is easier to read as for (const auto& [key, value] : map).

#include <iostream> #include <utility> struct point { int x; int y; }; int main() { point p{3, 4}; auto [x, y] = p; std::pair<int, int> pair{5, 6}; auto [a, b] = pair; std::cout << x + y << ' ' << a * b << '\n'; }

g++ -std=c++23 -Wall -Wextra structured.cpp -o structured
./structured

7 30

auto needs an initialiser

auto is not a type: it is a request to deduce one. Without an initialiser there is nothing to deduce from, and the declaration is an error.

GCC reports that the declaration of auto value has no initialiser, and points at the name. The fix is either an initialiser or an explicit type; there is no third option, because C++ has no untyped variable.

The same rule appears in a template parameter list and in an empty structured binding. When the compiler says it cannot deduce auto, the question to ask is which expression was supposed to supply the type.

#include <iostream> int main() { auto value; value = 3; std::cout << value << '\n'; }

g++ -std=c++23 -Wall -Wextra no_initializer.cpp -o no_initializer

no_initializer.cpp: In function 'int main()':
no_initializer.cpp:5:5: error: declaration of 'auto value' has no initializer
5 | auto value;
| ^~~~

Exercises

  1. Declare a const int, copy it into an auto variable and try to change the copy, then into a const auto and try again. Which line fails?
  2. Print sizeof for decltype(values[0]) and decltype((values[0])) where values is a std::vector. What does the extra pair of parentheses change?
  3. Use a structured binding with auto& to swap the two members of a point through the bound names.

Questions

A new set of questions in a new order every time. Choose one answer for each, then check.

  1. What does auto deduce for const int& value = x; auto copy = value;?

    Answer

    int, a copy without the const or the reference

    The deduction drops the reference and the top-level const, which is why auto is called the value type.

  2. What does auto& keep that plain auto drops?

    Answer

    The reference to the original object

    The reference in the declaration is part of the type, so the deduced variable refers to the original.

  3. What does decltype(expression) give?

    Answer

    The type of the expression, without evaluating it

    The operand is not evaluated; decltype is the compile-time type query.

  4. What does a structured binding do?

    Answer

    It names the parts of a pair, tuple or struct in one declaration

    The names refer to the elements, which makes auto [key, value] : map readable.

  5. What does this program print? #include <iostream> int main() { auto value = 4; auto& alias = value; alias *= 2; bool check = (value == 8); auto real = 7; auto ratio = 2.5; auto letter = 'x'; std::cout << value << ' ' << alias << ' ' << check << " / " << real << ' ' << ratio << ' ' << letter; }

    Answer

    8 8 1 / 7 2.5 x

    auto deduces the value type, the reference doubles through alias, and the print shows the deduced kinds.

  6. What does this program print? #include <iostream> #include <vector> int main() { std::vector<int> values{1, 2, 3}; for (auto& value : values) value *= 2; for (int value : values) std::cout << value << ' '; }

    Answer

    2 4 6

    The reference in the loop modifies each element, and the second loop prints the doubled values.

  7. What does this program print? #include <iostream> #include <vector> int main() { const std::vector<int> values{5, 4}; for (const auto& value : values) std::cout << value << ' '; }

    Answer

    5 4

    The const reference reads the two elements without copying the vector, and they print in order.

  8. What does this program print? #include <iostream> #include <type_traits> int main() { int value = 3; decltype(value) copy = value; decltype(3.5) ratio = 3.5; std::cout << value << ' ' << copy << ' ' << ratio << " / " << std::is_same_v<decltype(value), int>; }

    Answer

    3 3 3.5 / 1

    decltype names the type of the expression, so the copies keep int and double, and the trait is true.

  9. What does this program print? #include <iostream> #include <utility> int main() { std::pair<int, int> point{7, 30}; auto [first, second] = point; std::cout << first << ' ' << second; }

    Answer

    7 30

    The structured binding names the two members of the pair, and the program prints them in order.

  10. What does auto value; produce?

    Answer

    declaration of 'auto value' has no initializer

    auto needs an initialiser, because the initialiser is where the type comes from.

The code on this page was built with g++ 15.2.0 and run on Alpine Linux 3.24.3 (x86_64) in a Docker container. The programs whose section shows a compiler error were compiled until that error and were not run.

Article author: Arthur Isaev