Automatic Type Deduction with auto
Let the compiler infer variable types from their initializers with auto.
What Is Type Deduction for Objects?
Every object in C++ has a type, and up to now you have written that type out by hand. Type deduction lets you hand the job to the compiler instead: you write auto where the type would go, and the compiler works out what it should be by looking at the initializer.
The keyword itself does not describe a type. It is a placeholder that says "read the initializer and put the answer here". That means a definition like this one:
auto shippingRate { 0.62 };
is not a variable of some special flexible type. By the time the compiler is finished with that line, shippingRate is a double, exactly as if you had typed double yourself, and it will stay a double for the rest of its life. Nothing about it is decided at run time.
The motivation is that writing the type by hand often says the same thing twice. In double shippingRate { 0.62 }; the word double and the literal 0.62 both announce that this is a double. One of the two is redundant, and auto removes it.
One Rule Drives Every Case
Almost everything in this lesson follows from a single three-step recipe. When the compiler sees auto in a variable definition, it:
- Works out the type of the initializer expression.
- Strips the top-level
constoff that type. - Puts back whatever qualifiers you wrote yourself.
Read those steps once more before continuing. The parts of auto that surprise people are all step 2 and step 3 doing exactly what they say. Nothing else is going on.
auto is not a type and it is not a run-time mechanism. It is an instruction to the compiler to copy a type from one place in the line to another, with const removed in transit.
Step One: What Type Does the Initializer Have?
The initializer is an ordinary expression, so it already has a type by the ordinary rules you have been using all along. Deduction just reads it off.
#include <iostream>
int main()
{
auto parcelWeight { 12.75 }; // 12.75 is a double literal, so parcelWeight is a double
auto crateCount { 8 + 4 }; // 8 + 4 is an int expression, so crateCount is an int
auto palletCount { crateCount }; // crateCount is an int, so palletCount is an int
std::cout << parcelWeight << ' ' << crateCount << ' ' << palletCount << '\n';
return 0;
}
12.75 12 12
The third line is worth pausing on. crateCount was itself deduced, but by the time palletCount is defined there is nothing provisional about it. crateCount is an int, full stop, and that is the type palletCount picks up.
A function call is an expression too, so its return type feeds deduction the same way:
#include <iostream>
int computeFreight(int crates, int ratePerCrate)
{
return crates * ratePerCrate;
}
int main()
{
auto freightCost { computeFreight(12, 35) };
std::cout << "Freight cost: " << freightCost << '\n';
return 0;
}
Freight cost: 420
Because computeFreight() is declared to return int, freightCost is an int.
Since the initializer's type is what gets copied, a literal suffix on the initializer is an easy way to steer the result:
#include <iostream>
int main()
{
auto zoneCode { 7 }; // int
auto shippingRate { 0.62 }; // double
auto tareWeight { 2.5f }; // float, because of the f suffix
auto laneCode { 30u }; // unsigned int, because of the u suffix
std::cout << zoneCode << ' ' << shippingRate << ' ' << tareWeight << ' ' << laneCode << '\n';
return 0;
}
7 0.62 2.5 30
Step Two: Top-Level const Comes Off
This is the step that catches people. A const on the initializer does not survive the trip.
You can prove it without reading a single comment, because a variable that kept its const could not be assigned to:
#include <iostream>
int main()
{
const int maxCrates { 48 };
auto loadedCrates { maxCrates }; // deduced as int, not const int
loadedCrates = 51; // legal, because the deduced type carries no const
std::cout << "Capacity " << maxCrates << ", loaded " << loadedCrates << '\n';
return 0;
}
Capacity 48, loaded 51
The assignment compiles, so loadedCrates is a plain int. maxCrates is untouched and still const; only the deduced copy lost the qualifier.
This is less arbitrary than it looks. loadedCrates is a separate object that was initialized from a snapshot of maxCrates. Whether the original may be modified says nothing about whether the copy may be. Deduction therefore asks a narrower question than "what type is the initializer": it asks what type the value of the initializer is.
Step Three: Your Own Qualifiers Go Back On
Anything you write next to auto is applied after the strip, which is how you get a deduced type that is const after all:
#include <iostream>
int main()
{
const int maxCrates { 48 };
const auto reservedCrates { maxCrates }; // const stripped, then written back on
std::cout << "Reserved " << reservedCrates << " of " << maxCrates << '\n';
return 0;
}
Reserved 48 of 48
reservedCrates is a const int. Not because maxCrates was const, but because you wrote const on that line.
The next program is deliberately broken. Swapping in the assignment from the previous example now fails, because this time the variable really is const:
#include <iostream>
int main()
{
const int maxCrates { 48 };
const auto reservedCrates { maxCrates }; // const stripped, then written back on
reservedCrates = 51;
std::cout << reservedCrates << '\n';
return 0;
}
s.cpp: In function 'int main()':
s.cpp:8:20: error: assignment of read-only variable 'reservedCrates'
8 | reservedCrates = 51;
| ~~~~~~~~~~~~~~~^~~~
If you want a deduced variable to be
const, write const auto. Never rely on the initializer's const reaching the new variable, because it will not.
constexpr Is Not a Type
constexpr is a property of a declaration rather than part of a type, so there is nothing for deduction to pick up. What deduction does see is the implicit const that every constexpr variable carries, and step 2 removes that like any other.
#include <iostream>
int main()
{
constexpr double fuelSurcharge { 0.18 }; // type is const double; constexpr is not part of the type
auto adjusted { fuelSurcharge }; // double: the const came off
const auto lockedRate { fuelSurcharge }; // const double: we wrote the const back on
constexpr auto fixedRate { fuelSurcharge }; // const double, and still a compile-time constant
static_assert(fixedRate < 0.2);
std::cout << adjusted << ' ' << lockedRate << ' ' << fixedRate << '\n';
return 0;
}
0.18 0.18 0.18
All three variables hold the same value, and the static_assert succeeds, which is the proof that fixedRate really is usable at compile time. Note the division of labour in the constexpr auto definition: auto supplied double, and constexpr supplied both the compile-time guarantee and the const that comes with it.
When Deduction Has Nothing to Work With
Step one needs an expression with a type. Take that away and the whole recipe stalls. The following program is broken in three separate ways and will not compile:
#include <iostream>
void unloadDock()
{
}
int main()
{
auto berthNumber; // no initializer at all
auto laneWidth { }; // initializer is empty
auto status { unloadDock() }; // initializer has type void
return 0;
}
Trimmed, the compiler reports:
s.cpp:9:5: error: declaration of 'auto berthNumber' has no initializer
s.cpp:10:22: error: direct-list-initialization of 'auto' requires exactly one element [-fpermissive]
s.cpp:10:22: error: unable to deduce 'std::initializer_list<auto>' from '<brace-enclosed initializer list>()'
s.cpp:11:10: error: deduced type 'void' for 'status' is incomplete
Each diagnostic names a different failure. berthNumber has no initializer to read. laneWidth has braces with nothing inside them, so there is no expression at all. status has an initializer whose type is void, and void is an incomplete type that no object may have.
The mention of
std::initializer_list in the second diagnostic is the compiler explaining the one situation in which braces around a list would have meant something. With auto and braces, the rule is that exactly one element is expected and its type is what gets deduced. Empty braces give it nothing to count.
The String Literal Exception
Deduction reads the initializer's real type, and the real type of a string literal is not what most people expect. It is not std::string.
#include <iostream>
int main()
{
auto carrier { "Northline Freight" };
std::cout << carrier << '\n';
return 0;
}
Northline Freight
The program prints what you would hope, but carrier has type const char*. That is a leftover from C, and it is why auto and string literals are an awkward pair: the deduced type is a low-level handle into read-only memory rather than a string object with member functions. You will meet that type properly in the chapter on pointers.
When you want a real string type, put a suffix on the literal so that step one has the right thing to read:
#include <iostream>
#include <string>
#include <string_view>
int main()
{
using namespace std::string_literals;
using namespace std::string_view_literals;
auto portName { "Rotterdam"s }; // "Rotterdam"s is a std::string literal
auto laneLabel { "express"sv }; // "express"sv is a std::string_view literal
std::cout << portName << " via " << laneLabel << '\n';
return 0;
}
Rotterdam via express
Writing
auto portName { "Rotterdam"s }; works, but it is fair to ask what it buys you. The suffix already commits to a type, so the line is not shorter in any meaningful sense than std::string portName { "Rotterdam" };, and the second version names the type where a reader will look for it. Deduction from string literals is usually a case for writing the type out.
Choosing Between auto and a Written-Out Type
Rather than a list of pros and cons, it helps to ask one question: does anything in the surrounding code depend on knowing this variable's type at a glance? If not, deduction is a safe way to reduce noise. If it does, spell the type out.
| Situation | What to write | Reason |
|---|---|---|
| The type is plainly visible in the initializer | auto |
Naming it again adds nothing |
| The type name is long and you would just be copying the right-hand side | auto |
Less typing, fewer typos, and it cannot drift out of sync |
| You want a type that differs from the initializer's | explicit type | Deduction cannot give you a type the initializer does not have |
The variable feeds arithmetic where int versus double changes the answer |
explicit type | The reader needs to see which one it is |
| The variable is unsigned and will meet signed values | explicit type | Signedness is exactly the thing you do not want hidden |
| The initializer is a string literal | explicit type | auto gives you const char*, not a string class |
Two smaller benefits are worth naming because they are easy to miss.
The first is that deduction cannot produce an uninitialized variable. The second line below will not compile, and that is the point:
int berthCount; // compiles, but holds whatever happened to be in that memory
auto dockCount; // will not compile: there is nothing to deduce from
Getting into the habit of auto means the compiler catches the omission for you rather than leaving you a variable full of garbage.
The second is that deduction never inserts a conversion you did not ask for, because there is no target type for the value to be converted into. Compare these two definitions:
#include <iostream>
#include <string>
#include <string_view>
std::string_view manifestHeader()
{
return "PORT,LANE,CRATES";
}
int main()
{
std::string copied { manifestHeader() }; // builds a whole new std::string
auto viewed { manifestHeader() }; // stays a std::string_view
std::cout << copied << '\n';
std::cout << viewed << '\n';
return 0;
}
PORT,LANE,CRATES
PORT,LANE,CRATES
Both lines print the same text, but the first one copied every character into a fresh object. Writing a type on the left is an instruction to produce that type, and the compiler will convert to reach it. Writing auto asks for whatever you already have.
Use type deduction when the object's type does not matter to the code around it. Write the type out when you need a specific type that differs from the initializer's, or when the reader has to see the type to follow what the code does.
Where auto Hides a Bug
The cost of deduction is that the type is no longer written anywhere you can see. That matters most when two plausible types behave differently, and integer division is the classic case.
The next program is wrong. It divides fifteen crates across four trucks and reports the wrong number:
#include <iostream>
int main()
{
auto totalCrates { 15 };
auto trucksAvailable { 4 };
std::cout << totalCrates / trucksAvailable << '\n';
return 0;
}
3
The correct answer is 3.75. Both variables were deduced as int, so / performed integer division and discarded the remainder. Nothing on the line marked the operands as integers, so there is nothing to alert a reader. Had the definitions read double totalCrates { 15 };, the mistake would have been visible where it was made.
The fix is to give step one something floating-point to read:
#include <iostream>
int main()
{
auto totalCrates { 15.0 }; // the .0 is what makes this a double
auto trucksAvailable { 4 };
std::cout << totalCrates / trucksAvailable << '\n';
return 0;
}
3.75
There is a related hazard with no example to show, because it produces no visible symptom until later. A deduced variable's type is tied to its initializer, so if the initializer changes, the variable changes with it. Widen a function's return type from int to double and every auto variable initialized from a call to it silently becomes a double too. Sometimes that is exactly what you wanted. Sometimes it quietly changes the behaviour of code far away from the edit.
Looking Forward
This lesson covered deduction for objects whose types are plain values. The rules gain a second half once references and pointers are in play, since a reference has to decide whether to deduce the reference or the thing referred to, and that is where the const dropping rule earns its "top-level" qualifier. The lesson on type deduction with pointers, references, and const takes that up. The next lesson applies the same keyword in a different position, deducing a function's return type from its return statements.
Key Terminology
- Type deduction (also type inference): the compiler determining an object's type from its initializer
auto: the placeholder written in place of a type to request deduction- Initializer: the expression that gives a variable its starting value, and the only thing deduction has to work from
- Top-level
const: theconstthat applies to the object itself, which deduction removes - Incomplete type: a type such as
voidwhose size is unknown, so no object may have it - Literal suffix: a marker such as
f,u,s, orsvthat sets a literal's type, and therefore the deduced type
Summary
autois a placeholder, not a type. The compiler replaces it with a real type at compile time, and the variable's type never changes afterwards.- Deduction works in three steps: read the initializer's type, strip the top-level
const, then apply any qualifiers you wrote yourself. - Any expression can be the initializer, including arithmetic, another variable, and a call to a non-void function.
- Literal suffixes such as
f,u,s, andsvchange the initializer's type and therefore the deduced type. - The dropped
constis whyconst autoexists. Write theconstyourself if you want the new variable to have one. constexpris not part of the type system and cannot be deduced. Aconstexprvariable is implicitlyconst, thatconstis dropped like any other, andconstexpr autorestores it while also keeping the compile-time guarantee.- Deduction requires something to deduce from. No initializer, empty braces, and a
voidinitializer are all compile errors. - A string literal deduces to
const char*. Use thesorsvsuffix, or write the type out, when you wantstd::stringorstd::string_view. - Deduction rules out uninitialized variables and unrequested conversions, and it lines variable names up on the page.
- Its cost is hidden type information: integer division, signed versus unsigned, and a type that shifts when its initializer's type is edited are all easier to miss.
- Prefer
autowhen the type is not load-bearing for the reader, and write the type out when it is.
How did you find this lesson?
Your rating helps us improve the content.
Create an account to track your progress and access interactive exercises. Already have one? Sign in.
Automatic Type Deduction with auto - Quiz
Test your understanding of the lesson.
Practice Exercises
Type Deduction with auto
Practice using the auto keyword for automatic type deduction. Learn when auto improves code clarity and when explicit types are better.
Lesson Discussion
Share your thoughts and questions
No comments yet. Be the first to share your thoughts!