← Go back

C++: Return Type "auto"

2 years 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);
}