C++ auto keyword makes your life easier

Introduction

Since C++11, a compiler can deduce the type of a variable being declared from its initializer using auto keyword. It is also called placeholder type specifier.

C++ is a statically-typed language, therefore, auto turns to a specific type at compile-time (see Compile-time vs runtime post).

General Behavior

auto infers the type of a variable from its initializer. The variable gets a copy of the initializer

int i;
auto j = i; // j is int contains a copy of i

auto deduces reference type as a value type. To enforce being a reference, use auto&

int m = 0;
int& i = m; // i is an alias to m
auto j = i; // j is int contains a copy of i
auto& k = m; // k is int& and alias to m

auto can Not infer const qualifier. To enforce that, use const auto

int i =0;
const int m = 0;
auto j = m; // j is int (non-const)
const auto k = m; // k is const int
const auto l = i; // l is const int, holds a copy of i

auto& infers const qualifier

const int i =0;
auto& j = i; // const int&, alias for i

Literals

auto figures out the below types:

auto i = 1; // int
auto j= 7ul; // unsigned long
auto x = 2.0; // double
auto y = 2.0f; // float
auto c = 'A'; // char
auto s = "hi"; // char const*
auto b = true; // bool

From C++14, we can have std::string literals:

using namespace std; // This is necessary
auto s = "hellow"s // std::string, note the s operator. 

From C++23, we can have size_t and signed size_t:

auto k = 1uz; // size_t
auto m = 1z; // signed size_t

Classes

Custom classes are also inferred:

class LongNameClass {};
auto a = new LongNameClass(); // a is LongNameClass*

List initializer:

auto l = {1,2,3}; // l is std::initializer_list<int>

Long name types can be easily replaced by auto:

std::map<std::string, std::string> m; 
auto n = m;

auto p = std::make_unique<int>(); //unique pointer

std::vector<double> vec(100);
auto& r = vec;

A vector iterator type can be hidden with auto

// type of i is std::vector<double>::iterator
for (auto i = vec.begin(); i != vec.end(); i++){
    cout<< *i;
}

The elements of a vector can be easily read:

std::vector<unsigned long long int> vec = {1, 2, 3};

for (auto& x : vec)
    cout << x ;

Pointers

A pointer type is also deduced:

int i;
auto p = &i; // int*
auto q = p;  //int*
auto r = new int[5]; // int*

We can emphasize pointer type with * for readability. The outcome of the example below is the same as the previous one:

int i;
auto* p = &i; // int*
auto* q = p; // int*

Note both const auto and auto const are turned to a constant pointer with a mutable target. We still can customise the outcome pointer with *:

const auto r = &i; // int* const: constant pointer, mutable target
auto const s = &i; // int* const
auto* const t = &i;// int* const
    
const auto* u = &i;// const int*: mutable pointer, constant target
auto const* v = &i;// const int* or int const* 

const auto* const w = &i;// const int* const: pointer & target constant

References

By default, auto deduces reference as the value type

int y;
int& p = y;
auto x = p; // x is int and contains a copy of p

This value copy behavior can dramatically create a problem if you just want to create an alias to a big size vector. To overcome it, we have two options: auto& or decltype(auto) since C++14

int y;
auto& x = y; // x is an alias to y
decltype(auto) z = (y); // z is an alias to y

Note, we should do the same when initializing with a function

int& f(int& i){ return ++i;};
int m=0;
auto  j = f(m);// j is int but a copy return of f
auto& k = f(m);// k is int& and an alias to return of f
decltype(auto) l = f(m); // l is int& and an alias to return of f

Const

Using bare auto, const is not inferred. We have to use auto&, const auto, const auto& or decltype(auto)

const int m = 0;
auto j = m; // j is int
auto& k = m; // k is const int&
const auto l = m // l is const int
const auto& n = m // n is const int& 
decltype(auto) o = m // o is const int

auto vs decltype(auto)

In the examples shown in this post, we understand that auto alone cannot convert to a constant or reference type. To do so, we should use auto& or const auto. They give the programmer more control over the declared type.

int& f(int& i){return ++i;}
int x=10;
auto i = f(x);// i gets a copy of f(10)
auto& j = f(x);// j is a reference to x

But sometimes, like a wrapper, we want the compiler to deduce the type exactly as it is:

decltype(auto) FindTaxReturn(double& data){
    return Compute(data);
}

auto vs decltype

auto deduces the type of a variable when declared with the help of its initializer

auto i = 1; // int

decltype infers the type of expression which can be used for variable declaration or injection into a template

int f(){return 0;}
decltype(f()) i; // i is integer
vector<decltype(f())> v; // vector<int>, cannot be done with auto

Note that f() within decltype(f()) is not called. In fact during compilation and before the program is run, the declarations are concluded to be int i, vector<int> v.

Structured binding

A tuple can be decomposed with new variables like (C++17):

#include<tuple>

using namespace std;
int main()
{
    auto t = make_tuple(1, string{"hi"}, 2.1);
    auto& [id, message, value] = t;
}

Where id message and value are declared as int, string, and double referring to the tuple components. This is a good way to name members of a tuple for an easy to read code.

The members of a struct object can also be referred (or copied) via structured binding:

struct Employee{
    std::string Name;
    int Ids[2];
};

int main()
{
    Employee e{.Name="Jack", .Ids={7,9}};
    auto& [name, ids] = e;
    std::cout<< name <<" "<<ids[0] << ids[1];
}

Test

A good IDE like VS Code is your best friend to assess the outcome of auto. However, you can check the types using metafunctions in <type_traits> header:

#include <type_traits>
using namespace std;

int main(){
  auto x = true;
  cout<<is_same<decltype(x), bool>::value;
  return 0;
}

To ensure the type is correctly inferred, we can implement static_assert. It throws a compile-time error if the type is not correct:

static_assert(std::is_same<decltype(x), bool>::value, "x must be bool");

Another solution to check types is to use typeid as below.

#include <iostream>
#include <typeinfo>

int main()
{
    auto x=1.0u;
    std::cout<<typeid(x).name()<<std::endl;

    return 0;
}

The printed output is compiler-dependent. GCC on my machine produces strings like i, m, or very long texts. To interpret them, use command c++filt -t in the terminal. So for j output, we run

c++filt -t j

It produced unsigned int for me. typeid is not as reliable as typetraits functions, read here to for more info.

Function

At function definition, the type can be inferred with the aid of its trailing return type (C++14)

auto add(int a, int b){
    return a + b; // return type is int
}

Therefore, we have to guide the compiler if a function is only declared but not defined

auto subtract(int a, int b); // Error: cannot figure out return type
auto multiply(int a, int b) -> int; // works fine 

The compiler must be notified when the return type is ambiguous. In the example below, the compiler cannot deduce the type of f, that’s why we mention it as -> double.

auto f(bool cond) -> double
{
    if (cond)
        return 1;  // returns int
    else
        return 2.5; // returns double
}

However, a compile-time if constexpr can be used for a function which returns different types:

#include<iostream>

template<bool cond>
auto g() 
{
  if constexpr (cond)
    return 1;  // returns int
  else
    return std::string{"Hello"}; // returns string
}

int main(){
    std::cout<<g<false>(); // hello
    std::cout<<g<true>(); // 1
}

For more on constexpr see this post.

C++20 lets us have auto function arguments. The function below adds containers that have size() method, [] operator, and + operator for their elements. The outcome type is automatically promoted, for example, vector<int> plus vector<double> is vector<double>.

#include<iostream>
#include<vector>
#include<array>
using namespace std;

auto add (const auto& a1, const auto& a2)
{
    auto n =  a1.size();
    vector<decltype(a1[0] + a2[0])> result(n);
	for (decltype(n) i = 0; i < n; i++)
		result[i] = a1[i] + a2[i];
	return result;
}

int main(){
    
    array<int,3> a = {1,2,3};
    vector<double> b = {3.1,2.1,1.1};
    auto r = add(a,b);
    for (auto& item:r)
        cout<<item<<endl; // 4.1 4.1 4.1
    
    return 0;
}

What is auto&&

auto&& is a universal reference which can be lvalue reference or rvalue reference:

#include<type_traits>

using namespace std;
int main()
{
  int i=1;
  auto&& x{i};
  auto&& y{2};

  cout<<is_same_v<int&,decltype(x)>; //true
  cout<<is_same_v<int&&,decltype(y)>; // true
}

I have two comprehensive posts on this subject: move semantics and perfect forwarding.

concepts for auto

Sometimes auto needs to be constrained, then we use a concept:

#include<concepts>

std::floating_point auto x=1.1;
std::floating_point auto y=1.1f;

But this is an ERROR

std::floating_point auto z=1; // Error: 1 is not floating number

So the below function only works with floating-point types like float, double and so on:

void f(std::floating_point auto x){
    /* some code */;
}

Read more on conpepts in this post.

auto in header file

A function with auto argument is acceptable in a header file (C++20). A function, which returns auto, must declare return type with -> operator. The example below is on GitHub.

// a.h
template<class T>
struct A
{
    T x;
    auto Add(auto a)->decltype(a+x);
};

Note that the type of the function argument is relevant just to the function scope, but the type it returns affects the scope the function is called in. That’s why the header must hint at the return type.

In the .cpp file, all the necessary classes must be instantiated:

// compiler GCC 10.3 flag -std=c++20
// a.cpp
#include "a.h"

template<class T>
auto A<T>::Add(auto a)->decltype(a+x){
    return a+x;
}

// intantiate classes
template
auto A<double>::Add(double a)->decltype(a+x);

template
auto A<int>::Add(int a)->decltype(a+x);

// main.cpp
#include <iostream>
#include "a.h"
int main(){
  A<double> a1{.x=10};
  A<int> a2{.x=10};
  A<int> a3{.x=10};
  
  auto b = a1.Add(1.0); // OK. From header, compiler knows b is double

  std::cout<<a1.Add(20.0)<<std::endl; // OK
  std::cout<<a2.Add(20)<<std::endl; //OK
  std::cout<<a3.Add(20.0)<<std::endl; // Error: double A<int>::Add<double>(double) not found!

  return 0;
}


Lambda

auto is perfect for specifying the type of lambda functions.

auto l = [](int i) { return i + 1; };

Templates

auto can infer the return type of a templated function (C++14).

template<class T, class U>
auto add(T t, U u) 
{ return t + u; } // during compilation, type of t+u is deduced.  

If the return type is not clear for the compiler, we can help it with decltype.

template<class T, class U>
auto f(T t, U u, bool cond) -> decltype(t+1) // tell compiler, return type is of t+1
{
    if (cond)
        return t+1;
 
    return u+1;
};

Subscribe

I notify you of my new posts

Latest Posts

Comments

0 comment