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;
};
Latest Posts
- A C++ MPI code for 2D unstructured halo exchange
- Essential bash customizations: prompt, ls, aliases, and history date
- Script to copy a directory path in memory in Bash terminal
- What is the difference between .bashrc, .bash_profile, and .profile?
- A C++ MPI code for 2D arbitrary-thickness halo exchange with sparse blocks