Type Deduction
If you have been following the advice of Herb Sutter to almost always use auto, then you will have had much experience with type deduction. For the most part, type deduction just works as you'd expect, but there are cases where things might snag you.
Here is my template for the following explanation:
template<typename T>
void fun(ParamType t);
fun(expr);
In the following explanation, I will refer to expr
as the expression passed to the function. T
as the deduced type of the function, and ParamType
as the adorned type used as the argument for the function. For example:
template<typename T>
void fun(const T& t);
fun(10);
expr
is 10
which has a type of int
, ParamType
is const T&
, and T
will be deduced to int
.
Type Deduction Rules:
- Modifiers specified in
ParamType
are lost by the type deduction ofT
- So if
ParamType
isT&
, any lvalue references passed in to the function will have a deduced type which doesn't include the reference. So passing inconst int&
,T
will beconst int
. In this example, rvalue references would not be able to be bound. - If
ParamType
isconst T&
, then passing in aconst int
,T
will be deduced to beint
, notconst int
- So if
- If
expr
is a reference, the reference part is ignored - If
ParamType
is a universal reference andexpr
is a lvalue thenT
is deduced to be an lvalue references. This is the only time type deduction deduces a reference - If
ParamType
is justT
(pass-by-value) constness, referenceness, and volatility is ignored. - If
ParamType
is a reference, andexpr
is an array, thenT
deduces the array's type. It doesn't decay into a pointer like normal. This applies to function pointers as well
auto
type deduction mostly follows the same rules as above. You can think of auto
as the T
in template type deduction.
One caveat is while template type deduction cannot deduce braces, auto
type deduction can, and it will deduce as std::initializer_list
.
A function that returns auto
follows template type deduction rules, not auto
type deduction rules.
The best way to understand this is with examples:
// When I saw 'T is', I mean to say that 'T is replaced by'
void foo(T t);
void bar(const T& t);
void func(T&&);
std::vector<double> d;
const int c;
volatile bool b;
foo(5); //T is int
foo(c); //T is int
bar(c); //T is int
bar(5); //T is int
bar(b); //T is volatile bool
bar(d); //T is std::vector<double>
func(c); //T is const int&
func(d); //T is std::vector<double>&
func(55); //T is int
bar(std::string("...")); // error, cannot bind to rvalue reference
// Once again when I say "auto becomes", I mean to say that "auto is replaced by" as if
// auto was "T" in the above example
std::vector<double> d;
const int c;
volatile bool b;
auto& f = c; //auto becomes const int
auto&& d2 = d; //auto becomes std::vector<double>&
// auto&& is a universal reference
const auto v = b; //auto becomes bool
const auto& v2 = b; //auto becomes volatile bool
//when I mean "auto becomes" I essentially
//mean it's as if you typed this:
const volatile bool& v2 = b;
auto initList = {10, 20, 30, 40}; //std::initializer_list<int>
std::vector v = {10, 20, 30, 40}; //std::vector<int>
auto vec = std::vector{10, 20, 30, 40};
auto& v2 = v;
// auto becomes std::vector<int>
// so v2 is an std::vector<int>&
The decltype
rules are very simple.
It produces the exact type of the expression passed.
We can use decltype
rules in place of auto
or template rules for variables and return values with the syntax decltype(auto)
.
auto operator[](int index) {
return c[index];
// template type deduction rules
// since auto lacks any reference, it is returned by-value
// so we can't use operator[] to assign values
}
auto& operator[](int index) {
return c[index];
// returns lvalue reference
}
auto&& operator[](int index) {
return c[index];
// returns lvalue reference
// auto&& is a universal reference
}
decltype(auto) operator[](int index) {
return c[index];
// returns lvalue reference
// return type of operator[] is an lvalue reference
}
int c = 100;
auto d = c; // auto is int
decltype(c) d = c; // type is int
decltype(auto) e = c; // type is int
int& get(int & i) {
return i;
}
auto f = get(c); // auto is int
decltype(auto) g = get(c); // type is int&
int i;
int&& f();
auto x3a = i; // decltype(x3a) is int
decltype(auto) x3d = i; // decltype(x3d) is int
auto x4a = (i); // decltype(x4a) is int
decltype(auto) x4d = (i); // decltype(x4d) is int&
auto x5a = f(); // decltype(x5a) is int
decltype(auto) x5d = f(); // decltype(x5d) is int&&
auto x6a = { 1, 2 }; // decltype(x6a) is std::initializer_list<int>
decltype(auto) x6d = { 1, 2 }; // error, { 1, 2 } is not an expression (only auto deduces braces to initializer list)
auto *x7a = &i; // decltype(x7a) is int*
decltype(auto)*x7d = &i; // error, declared type is not plain decltype(auto)
So with this knowledge, let's look at the following example:
template<typename T>
auto make_unique_cpy(T&& t) {
using Type = std::remove_reference_t<T>;
return std::unique_ptr<Type>(new Type(std::forward<T>(t)));
}
If a lvalue is passed to make_unique_simple
, then T
will be deduced to a lvalue reference.
Since we can't create a pointer to a reference, we must use std::remove_reference_t
to ensure that the type being passed to unique_ptr
and new
is not a reference.
During type deduction, there may be cases where a reference to a reference is produced.
Since such double references are illegal, the compiler follows the rules of reference collapsing to produce a single reference.
This can occur when using decltype
, type aliases, or during type deduction, for example. When a reference to a reference is produced:
- If either references is a lvalue references, the result is a lvalue reference
- Otherwise, the expression collapses to a rvalue reference.
template<typename T>
auto func(T&& param) {
const T&& p2 = param;
// since const is used
// p2 is not a universal reference
}
std::string name = "Hello";
func(name);
// T deduced to std::string&
// type of p2 becomes const std::string& &&
// type of p2 collapses to const std::string&
func(10);
// T deduced to be int
// type of p2 becomes const int &&