C++: Return Type "auto"
a year ago by Lukas Kerkemeier
TL;DR: auto f() {...}
will never return a reference. Use decltype(auto) f() {...}
if a reference is needed. And even then you will need to explicitly state that it should be a reference.
See this for more information.
The Problem
Imagine you have the following code:
auto f() {
static int i = 0;
std::cout << i;
return i;
}
[...]
f()++;
f();
What will the program print? You might think that it is 01
but that is not true. This program doesn't even compile because f
returns by value.
One possible idea to fix the problem is to use decltype(auto)
. It can be understood as "perfect forwarding" a return value. However, the code
decltype(auto) f() {
static int i = 0;
std::cout << i;
return i;
}
[...]
f()++;
f();
still does not compile.
This is the case, because the decltype(auto)
works on the "thing" that is returned. And decltype(i)
on the entity i
is the type of i
, which is int
. If you replace it with decltype((i))
it's on the expression (i)
, which is int&
. So, with this knowledge we can fix the code to be
decltype(auto) f() {
static int i = 0;
std::cout << i;
return (i);
}
[...]
f()++;
f();
This now works and prints 01
.
Let's have a look at an example that is a bit more complicated:
int& f() {
static int i = 0;
return i;
}
decltype(auto) g() {
return f();
}
[...]
f()++;
std::cout << f();
This compiles and prints 1
. The decltype(auto)
of g
evaluates decltype(f())
which evaluates to the return type of f
, which is int&
.
The Problem, Level 2
Let's say you want to save a reference to a local variable. The problem above also applies in this case:
int& f() {...}
[...]
auto a = f(); // int a
auto& b = f(); // int& b
decltype(auto) c = f(); // int& c
Example Code
There is code attached that shows a few different variations on this and prints out if it would compile, and what return value can be expected. CAUTION: These cases are a bit more specific then the ones in the text above because the return value depends on the parameter type that is a reference.
#include <iostream>
#include <string>
#include <type_traits>
auto plainAuto(int &a) { return a; }
auto trailingAuto(int &a) -> decltype(a) { return a; }
auto trailing2Auto(int &a) -> decltype((a)) { return a; }
decltype(auto) declAuto(int &a) { return a; }
template <typename F> void evaluate(F f, std::string name) {
static constexpr bool is_ref_type =
std::is_reference_v<std::invoke_result_t<F, int &>>;
std::cout << name << ": " << (is_ref_type ? "" : "no ") << "reference\n";
int toRef = 0;
auto a = f(toRef);
bool a_res = std::is_reference_v<decltype(a)>;
bool b_res = false;
if constexpr (is_ref_type) {
auto &b = f(toRef);
b_res = std::is_reference_v<decltype(b)>;
}
std::cout << " auto a -> "
<< "int " << (a_res ? "&" : " ") << "a" << '\n'
<< " auto &b -> "
<< " " << (b_res ? "int &b" : "compile error") << '\n';
}
#define EVALUALTE(X) evaluate(X, #X)
int main() {
EVALUALTE(plainAuto);
EVALUALTE(trailingAuto);
EVALUALTE(trailing2Auto);
EVALUALTE(declAuto);
}