Function Return Type Deduction
Let the compiler deduce function return types from return statements.
What Is Type Deduction for Functions?
Type deduction for functions lets the compiler work out a function's return type from the return statements in its body. You write auto where the return type would normally go, and the compiler fills in the answer.
To see why this is possible at all, look at what the compiler already does for an ordinary function. A wheel builder's shop keeps a running total of spoke tension across a wheel:
int totalTension(int perSpoke, int spokes)
{
return perSpoke * spokes;
}
The compiler works out that perSpoke * spokes has type int, then checks that this type either matches the declared return type or can be converted to it. The type of the return expression is computed either way. Return type deduction simply keeps that answer instead of throwing it away:
auto totalTension(int perSpoke, int spokes)
{
return perSpoke * spokes;
}
The return statement yields an int, so the function's return type is int. Not "any type", and not a type decided at run time: a single, fixed type baked in at compile time, exactly as if you had typed it yourself.
Deduced return types are a C++14 feature. Before that, `auto` in the return position was only ever half of the trailing return syntax described later in this lesson. Compiling the second example above with `-std=c++11` makes gcc say so directly: `note: deduced return type only available with '-std=c++14' or '-std=gnu++14'`.
Deduction Adopts the Expression's Type, It Does Not Convert To It
The difference between writing a return type and deducing one is about who wins when the two disagree. A declared return type is a target the result gets converted to. A deduced return type is a copy of what the body actually produced.
Spoke length starts from the gap between the rim's effective radius and the hub flange radius, which is a fractional number of millimetres:
#include <iostream>
// the declared type wins: the double result is converted to int on the way out
int spokeReachInt(double rimRadius, double hubRadius)
{
return rimRadius - hubRadius;
}
// auto adopts the type of the expression, so no conversion happens
auto spokeReachAuto(double rimRadius, double hubRadius)
{
return rimRadius - hubRadius;
}
int main()
{
std::cout << "declared int: " << spokeReachInt(307.5, 21.75) << " mm" << '\n';
std::cout << "deduced auto: " << spokeReachAuto(307.5, 21.75) << " mm" << '\n';
return 0;
}
This prints:
declared int: 285 mm
deduced auto: 285.75 mm
The first function throws away three quarters of a millimetre, which is enough to build a wheel with the spokes bottoming out in the nipples. Nothing in the call site hints at it. This is the single strongest argument for deduction: a return type you did not write cannot disagree with the value you return, so the class of bug where a function quietly converts its own result on the way out simply cannot happen.
Every Return Statement Must Agree
A function has one return type, so deduction must arrive at one answer. If two return statements produce different types, there is no answer to arrive at.
The following function does not compile:
auto tensionTarget(bool rearWheel)
{
if (rearWheel)
return 118; // int
return 96.5; // double
}
The compiler reports:
s.cpp: In function 'auto tensionTarget(bool)':
s.cpp:6:12: error: inconsistent deduction for auto return type: 'int' and then 'double'
6 | return 96.5; // double
| ^~~~
Note that the compiler is not choosing the wider type, nor the type of the first return statement it saw. It deduced int from the first return, then found a second return that disagreed, and stopped.
There are two ways out, and they are opposites. Either name the return type, which puts the conversions back and makes the int become a double, or keep auto and make the return statements agree:
#include <iostream>
// fix 1: name the return type, and let the compiler convert the int result
double tensionTargetExplicit(bool rearWheel)
{
if (rearWheel)
return 118;
return 96.5;
}
// fix 2: keep auto, and make every return statement produce the same type
auto tensionTargetDeduced(bool rearWheel)
{
if (rearWheel)
return 118.0;
return 96.5;
}
int main()
{
std::cout << "explicit rear/front: " << tensionTargetExplicit(true)
<< " / " << tensionTargetExplicit(false) << '\n';
std::cout << "deduced rear/front: " << tensionTargetDeduced(true)
<< " / " << tensionTargetDeduced(false) << '\n';
return 0;
}
Output:
explicit rear/front: 118 / 96.5
deduced rear/front: 118 / 96.5
Changing the literal 118 to 118.0 was enough here. When the mismatched value is not a literal, static_cast does the same job at each return statement.
A Declaration Alone Cannot Be Deduced From
Deduction reads the function body. A forward declaration has no body, so there is nothing to read.
The following program does not compile, even though the definition appears later in the same file:
#include <iostream>
auto rimHoleCount();
int main()
{
std::cout << rimHoleCount() << '\n';
return 0;
}
auto rimHoleCount()
{
return 32;
}
The compiler reports:
s.cpp: In function 'int main()':
s.cpp:7:30: error: use of 'auto rimHoleCount()' before deduction of 'auto'
7 | std::cout << rimHoleCount() << '\n';
| ~~~~~~~~~~~~^~
Moving the definition above main() fixes it. The rule is positional: at the point of the call, the compiler must have already seen the body.
This rule quietly rules out deduced return types for anything you publish in a header. A header gives other files a declaration, and a declaration is exactly what deduction cannot work with. In practice, a normal function returning `auto` is callable only from the file that defines it.
When a Deduced Return Type Earns Its Place
Deduction costs you something too. A declaration is the part of a function other programmers read, and auto removes the return type from it. A reader has to open the body, or trust their editor, to answer a question the declaration used to answer on its own.
So the useful question is not "is deduction good", but "does this particular return type want to be written down". Two cases where it does not:
The type is awkward to name. Mixing narrow integral types runs the operands through the promotion and conversion rules covered earlier in this chapter, and the result is often not the type either operand started with:
#include <iostream>
// both operands are promoted before the addition, so the result is not a short
auto flangeSpan(unsigned char dishOffset, short rimOffset)
{
return dishOffset + rimOffset;
}
int main()
{
std::cout << "flange span: " << flangeSpan(38, 217) << " tenths of a mm" << '\n';
return 0;
}
Output:
flange span: 255 tenths of a mm
Guessing short here would be wrong, and guessing wrong reintroduces the silent conversion from the earlier example. auto sidesteps the guess.
The type is fragile. A fragile return type is one that a small edit to the body would change:
#include <iostream>
// the shop switched from 6 gram plain-gauge spokes to 5.4 gram butted spokes
constexpr double spokeMassGrams{ 5.4 };
auto spokeSetMass(int spokes)
{
return spokes * spokeMassGrams;
}
int main()
{
std::cout << "spoke set: " << spokeSetMass(32) << " g" << '\n';
return 0;
}
Output:
spoke set: 172.8 g
When spokeMassGrams was a whole number of grams, this function returned an int. Changing one constant changed the natural result type to double, and the deduced return type followed on its own. Had the return type been written as int, that same edit would have kept compiling and started truncating.
Here is the decision in table form:
| Situation | What to write |
|---|---|
| Declared in a header, called from other files | Explicit type. Deduction cannot cross that boundary at all. |
| Callers store, compare, or forward the result | Explicit type. The declaration should answer the question. |
| Result type falls out of promotion or conversion rules | auto. Naming it invites a wrong guess. |
| Result type would change if the body were tweaked | auto. It tracks the body for free. |
| Type is long to spell but still worth showing | Trailing return type, covered next. |
Prefer explicit return types. Reach for a deduced return type only when the type is unimportant to callers, difficult to express, or fragile. Unlike `auto` for objects, where the initializer sits right there in the same statement, there is no broad agreement that deduced return types are an improvement.
Trailing Return Types Are a Separate Feature
auto appears in a second, unrelated function syntax. Writing auto before the function name and -> Type after the parameter list is called the trailing return syntax:
#include <iostream>
auto tensionSpread(int lowKgf, int highKgf) -> int
{
return highKgf - lowKgf;
}
int main()
{
std::cout << "acceptable spread: " << tensionSpread(95, 125) << " kgf" << '\n';
return 0;
}
Output:
acceptable spread: 30 kgf
In this syntax `auto` deduces nothing. It is punctuation: a placeholder that holds the leading position open so the real return type can be given after the parameters. `auto tensionSpread(int, int) -> int` and `int tensionSpread(int, int)` declare exactly the same function.
Three reasons to use it:
Readability. With a long return type, the leading syntax buries the function name in the middle of the declaration. The trailing form puts the name first, so a reader who does not care about the return type never has to parse it.
Alignment. Because every declaration starts with the same four characters, the names line up:
auto spokeCount(int rimHoles) -> int;
auto averageTension(int totalKgf, int spokes) -> double;
auto printBuildSheet(std::string_view wheelName) -> void;
auto lacingPattern(int crossCount) -> std::string;
Return types that depend on the parameters. A leading return type is parsed before the parameter list exists, so it cannot mention a parameter. Recall that decltype(x) yields the type of x. The first declaration below does not compile:
// error: the parameters are not in scope yet at this point in the declaration
decltype(rimRadius - hubRadius) spokeReach(double rimRadius, double hubRadius);
gcc reports:
s.cpp:2:10: error: 'rimRadius' was not declared in this scope
2 | decltype(rimRadius - hubRadius) spokeReach(double rimRadius, double hubRadius);
| ^~~~~~~~~
A trailing return type is parsed after the parameter list, so the parameter names are in scope by then:
#include <iostream>
auto spokeReach(double rimRadius, double hubRadius) -> decltype(rimRadius - hubRadius);
auto spokeReach(double rimRadius, double hubRadius) -> decltype(rimRadius - hubRadius)
{
return rimRadius - hubRadius;
}
int main()
{
std::cout << "reach: " << spokeReach(307.5, 21.75) << " mm" << '\n';
return 0;
}
Output:
reach: 285.75 mm
Note that this declaration is perfectly usable from another file, because the return type is stated rather than deduced. Some later features, lambdas among them, require the trailing syntax outright. Until you meet one, the traditional leading return type is still the default.
auto in the Parameter List Means Something Else
A natural next guess is that if auto deduces the return type, it should also deduce parameter types:
#include <iostream>
void showAverageTension(auto totalKgf, auto spokes)
{
std::cout << "average: " << totalKgf / spokes << " kgf" << '\n';
}
int main()
{
showAverageTension(1080, 24); // case 1: two int arguments
showAverageTension(1148.4, 24.0); // case 2: two double arguments
return 0;
}
Under C++20 this compiles and prints:
average: 45 kgf
average: 47.85 kgf
The behaviour is right, but the explanation is not. Type deduction never applies to function parameters. What C++20 added here is an abbreviated way to write a function template, so the two calls above do not share one function at all: the compiler generates a separate showAverageTension for the int arguments and another for the double arguments. Compiling the same file as C++17 shows where the feature belongs:
s.cpp:3:25: warning: use of 'auto' in parameter declaration only available with '-std=c++20' or '-fconcepts' [-Wc++20-extensions]
3 | void showAverageTension(auto totalKgf, auto spokes)
| ^~~~
In C++17 and earlier there is no such thing as an auto parameter, and gcc accepts it only as a non-standard extension. Function templates are covered properly in a later chapter.
Summary
Deduced return types are the C++14 feature that lets you write auto in place of a return type. The compiler takes the type of the return expression and makes it the function's return type, at compile time, once and for all.
Deduction copies, it does not convert. A written return type converts the result to itself, which is how a double calculation silently becomes an int. A deduced type cannot disagree with the value returned, so that whole class of accident disappears.
One type per function. Return statements with different types produce error: inconsistent deduction for auto return type. Fix it by naming the return type, which restores the conversions, or by making every return statement produce the same type.
Deduction needs the definition. A forward declaration has no body to read, so a call before the definition fails with error: use of 'auto f()' before deduction of 'auto'. This is why deduced return types do not work through headers.
The cost is that a declaration no longer states what it returns, and the declaration is the interface most readers see.
Trailing return types (auto name(params) -> Type) are unrelated to deduction. The auto is a placeholder, the real type comes after the parameters. Use it for readability with long return types, for aligned declarations, when the return type must refer to a parameter via decltype, and where the language demands it.
auto parameters are not deduction. They were invalid before C++20; from C++20 they declare a function template, and each distinct set of argument types produces a distinct function.
Write return types explicitly by default. Deduce one only when it is unimportant, hard to express, or fragile, and remember that doing so confines the function to the file that defines it.
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.
Function Return Type Deduction - Quiz
Test your understanding of the lesson.
Practice Exercises
Type Deduction for Functions
Practice using auto return type deduction and trailing return types in functions. Learn when to use explicit return types versus letting the compiler deduce them, and understand the rules for type deduction.
Lesson Discussion
Share your thoughts and questions
No comments yet. Be the first to share your thoughts!