Type deduction
One of the most well-known features of C++11 is type deduction. This feature allows the compiler to infer the type of variables, functions, and expressions.
The two large features of type deduction introduced in C++11 and C++14 are auto
and decltype
.
In all examples, we will assume C++14, so don't forget to compile with the -std=c++14
flag.
auto
auto
was introduced in C++11 to automatically deduce the types of variables based on their initialization.
A simple use can be seen here:
#include <iostream>
int main() {
auto i = 42;
std::cout << "i is " << 42 << std::endl;
return 0;
}
Compiling this example and running the executable correctly outputs "i is 42". Excellent.
This is extremely useful when we have long, cumbersome types that we would prefer not to type. A classic example of this is the iterator
type for containers in the C++ Standard Library:
std::vector<some_type>::iterator it = v.begin();
What a pain to write over and over. Because auto
can deduce the type of it
from the return type of std::vector::begin
, we can simply write:
auto it = v.begin();
Consider the following example using auto
for an iterator:
#include <iostream>
#include <vector>
#include <string>
int main() {
std::vector<std::string> v;
v.emplace_back("Hello");
v.emplace_back("World");
for(auto it = v.begin(); it != v.end(); ++it) {
std::cout << *it << " ";
}
std::cout << std::endl;
return 0;
}
A note about the above code: we use std::vector::emplace_back
, which has a similar functionality to std::vector::push_back
. Instead of pushing an instantiated object, std::vector::emplace_back
will construct an object if the correct parameters to a constructor are passed. In this case, we have a std::vector<std::string>
. In order to construct a std::string
, we pass const char*
to emplace_back
and construct and "push" std::string
into v
.
One interesting feature of auto
is that it doesn't capture reference automatically. This forces the programmer to decide whether or not the object is going to be changed. The following example illustrates this:
#include <iostream>
#include <vector>
struct A {
A(int x) : x(x) {}
int x;
};
int main() {
std::vector<A> v;
// Construct three A objects with x values of 1, 2, and 3 and "push" them into v.
for(int i = 1; i <= 3; ++i) {
v.emplace_back(i);
}
// Here auto deduces the type of v[0] to be A, so a_by_value's type is A
auto a_by_value = v[0];
a_by_value.x = 100;
std::cout << "By value: \n";
for(auto it = v.begin(); it != v.end(); ++it) {
std::cout << it->x << std::endl;
}
// Here auto deduces the type of v[0] to be A, but we qualify it with a reference, so a_by_ref's type is A&
auto& a_by_ref = v[0];
a_by_ref.x = 100;
std::cout << "\nBy reference: \n";
for(auto it = v.begin(); it != v.end(); ++it) {
std::cout << it->x << std::endl;
}
return 0;
}
The above code produces the following output:
By value:
1
2
3
By reference:
100
2
3
This is what we expect, because in the "By value" section, we were changing a copy of v[0]
, but in the "By reference" section, we were changing a reference to v[0]
. This is what we expected to see, so this is good.
Similarly, auto
will not default to const
-qualifying variables:
const int get_42() {
return 42;
}
int main() {
auto x = get_42(); // x is of type int, so it is mutable.
const auto y = get_42(); // y is of type const int, so it is immutable.
x++; // Legal
// y++; // Illegal - y is read-only.
return 0;
}
Since C++14, auto
can also be used for function declarations and normally has a trailing return type:
#include <iostream>
#include <string>
auto get_greeting() -> std::string {
std::string s("Hello, World!");
return s;
}
int main() {
auto s = get_greeting();
std::cout << s << std::endl;
return 0;
}
If a trailing return type is omitted, normal auto
rules apply to the returned expression.
At first, this may seem silly. Why would we want to manually specify the return type of a function? Doesn't that defeat the purpose of using auto
in a function signature?
To answer this question, we will begin our discussion of decltype
.
decltype
decltype
was introduced in C++11 to automatically yield the declared type of an expression.
decltype
differs from auto
in that it will qualify references and const
if that is the declared type of an expression.
#include <iostream>
struct A {
const int x = 42;
int add_one_to_x() { return x + 1; }
};
int main() {
A a;
decltype(a.x) x = a.x;
// x++; // Illegal - x is immutable because type of x is const int
std::cout << "x is " << x << std::endl;
decltype(a.add_one_to_x()) x_plus_one = a.add_one_to_x(); // x_plus_one is of type int
std::cout << "x + 1 is " << x_plus_one << std::endl;
++x_plus_one; // Legal because x_plus_one is mutable
std::cout << "x + 1 + 1 is " << x_plus_one << std::endl;
return 0;
}
This outputs:
x is 42
x + 1 is 43
x + 1 + 1 is 44
This is a really powerful construct because now we don't need to know the return type of an expression to declare a variable of that type. Similarly, decltype
can be used for the return type of a function. This is where auto
is useful for functions.
Consider the trivial example where two objects are added:
#include <iostream>
#include <string>
template <typename T, typename U>
auto add(T t, U u) -> decltype(t + u) {
return t + u;
}
int main() {
std::string s1("Hello, "), s2("World");
auto x = add(1, 2.3);
auto y = add(s1, s2);
// auto z = add(s1, 1.0); // Fails - std::string + double is not defined
std::cout << x << std::endl;
std::cout << y << std::endl;
return 0;
}
This allows for the reuse of the function add
for any two objects that support the operator +
. Instead of overloading the function many times, we can simply use decltype
. Additionally, the program will fail to compile if operator +
is not defined on the two types, as seen in the section that is commented out.