Understand Auto Type deduction
To understand clearly Auto and how it works. we need first to go through a simple revision on an unlovely topic. the rvalue and lvalue
rvalue and lvalue
template
template-type deduction
Auto algorithm
rvalue and lvalue
a hint and funny definition are that the lvalue is the container and rvalue is the object to be kept in the container. then it is obvious the object will not live for long without a container or variable. if you can't explicitly tell the difference then you have to go and read my article for more understanding and examples.
Template
to be fully aware of the deep roots of the Auto implement then the template is a good start. remember the topic when you were first hit by a massive project where you built tons of classes and after a year you look back again to your code you found out the way you write was like a caveman uses a lighter to find a wood stick to light. you found out the golden rule where the code reusability is established. so you are now a civilized man who can use templates. so we all agree on the importance of templates in our life. another fun definition- I am funny to admit it in the comment. a template gives you the power to generalize your code to any kind of unexpected input. believe it or not, if you are not good at templates or you are not using them a lot then you are way too far from professional codes.
to understand the template again please, go and read my article- it is too briefly written to be where- I am going to help you in the future but not right now, sorry.
template-type deduction,
you may not have ever heard of this topic before, or even if it seems boring and it actually is. but surprisingly, it is kinda intuitive as a controversy to many C++ topics.
Things to Remember
During template-type deduction, arguments that are referenced are treated as non-references, i.e., their reference-ness is ignored.
When deducing types for universal reference parameters, lvalue arguments get special treatment.
When deducing types for by-value parameters, const and/or volatile arguments are treated as non-const and non-volatile.
During template type deduction, arguments that are array or function names decay to pointers, unless they’re used to initialize references.
seems harsh even to remember these bullet points. so to explain more we have three cases
ParamType is a pointer or reference type, but not a universal reference. (all you need to know is that they exist and that they’re not the same as lvalue references or rvalue references.)
ParamType is a universal reference.
ParamType is neither a pointer nor a reference.
Case 1: ParamType is a Reference or Pointer, but not a Universal Reference
if the expression's type is a reference, ignore the reference part.
then pattern-match expr's type against paratype to determine T.
yes yes, I know you skipped the above points and want to see a real example. fine!
template<typename t>
void f(T& param); //param is a refernce.
int main(){
int x=1; // x is an int
const int cx=x; // cs is a const int
const int& rx=x; // rx is a const refernce to x.
f(x); // T is int, param's type is int& easy?
f(cx); // T is const int, param's type is const int&
f(rx); // the same as above.
return 0;
}
what you can notice above the second and third is the same if we followed the above rules. when the callers pass a const object to a reference param they expect the object remains unmodifiable. that's why passing a const to a reference parameter is safe.
in the third test case, even though the rx is a reference type, T is deduced to be non-reference that's because the rx's reference-ness is ignored during type deduction.
the above example shows the lvalue, hmm what are we missing here? oh sheesh the rvalue. we change the type of f's param from T& to const T&. as const is part of the param no need for const to be deduced as part of T.
template<typename t>
void f(const T& param); //param is a refernce to const.
int main(){
int x=1; // x is an int
const int cx=x; // cs is a const int
const int& rx=x; // rx is a const refernce to x.
f(x); // T is int, param's type is const int& easy?
f(cx); // T is int, param's type is const int&
f(rx); // the same as above.
return 0;
}
as before the rx's reference-ness is ignored during type deduction. even if we changed the param to a pointer instead of ref things seem to react the same way.
it seems you are not making any progress of any type. but that's as I mention before this topic is so natural which is odd.
Case 2: ParamType is a Universal Reference
things behave differently, as the universal. for this there is a deep talk about the universal ref. and giving here is the headline.
If expr is an lvalue, both T and ParamType are deduced to be lvalue references. That’s doubly unusual. First, it’s the only situation in template-type deduction where T is deduced to be a reference. Second, although ParamType is declared using the syntax for an rvalue reference, its deduced type is an lvalue reference.
If expr is an rvalue, the “normal” (i.e., Case 1) rules apply.
template<typename t>
void f(const T&& param); //param is universal.
int main(){
int x=1; // x is an int
const int cx=x; // cs is a const int
const int& rx=x; // rx is a const refernce to x.
// lvaleus
f(x); // T is int&, param's type is const int& easy?
f(cx); // T is const int&, param's type is const int&
f(rx); // the same as above.
// rvalues
f(10); // now T is int, Param's type is int&&
return 0;
}
Case 3: ParamType is Neither a Pointer nor a Reference
you may use pass-by-value.so, what happens when you use this in the case of templates?
That means that the param will be a copy of whatever is passed in—a completely new object. The fact that param will be a new object motivates the rules that govern how T is deduced from expr:
As before, if expr’s type is a reference, ignore the reference part.
if after ignoring the expr's reference-ness, expr is const, ignore that, too. if it is volatile also ignore that.
// change the temp to be
void f(T param);
// calling the saame calls we did before
f(x); // T is int and param is int
f(cx); // T and param are int
f(rx); // T and param are int
I think I started to lose you in the way, don't worry we are that close. I forgot to update the topic to match the article opps, It happen by accident we drift to such an interesting topic. but admit it you felt the danger and the complicity of templates.
this marks the end of the topic of Template deduction, so far there are two additional detailed topic-truly that are so important but far away from the auto, we use- the array argument and function argument.
Understand Auto Type of deduction
once you are done with the above topics I think it is pretty easy to review the auto. yes, review as the auto is just another hard code of template deduction.
template<typename T> void func_for_x(T param); // conceptual template for
func_for_x(27); // deducing x's type
// conceptual call: param's
// deduced type is x's type
template<typename T>
void func_for_cx(const T param); func_for_cx(x); // conceptual template for // deducing cx's type// conceptual call: param's // deduced type is cx's type
template<typename T>
void func_for_rx(const T& param); // deducing rx's type
func_for_rx(x);
auto&& uref1 = x; // x is int and lvalue, so uref1's type is int&
auto&& uref2 = cx; // cx is const int and lvalue, so uref2's type is const int&
auto&& uref3 = 27; //27 is int and rvalue, so uref3's type is int&&
it is so familiar to the template type deduction. Actually, it follows the rules of all template deduction methods. but, as always there is a but-the case that is different.
we first introduced by a C++98 gives us two methods of declaring a var
int x=1;
int y(2);
now C++11 supports the new declaring method.
int x2={4};
int x3{3};
all on 4 syntaxes only one result to be int. so in all forms above if we replaced auto with the fixed type declaration- there are advantages though. then the result is not.
auto x=1; // the auto is normal is int.
auto x2(1); // ditto
auto x3={1}; // type is std::initaliazer_list<int>
// value is {27}
auto x4{1}; // ditto
auto x5 = { 1, 2, 3.0 }; // error! can't deduce T for // std::initializer_list<T>
so to treat the last error on the above code, what if we used the rvalue or pass-by-value
template<typename T>
void f(T param); // pass by value
f({1,3,4}) // error not able to deduce this type though that error doesn't seem to occure in case of auto
there is another approach to solving the problem where the hard code is implemented.
template<typename T>
void f(std::initializer_list<T> param);
f({1,3,4}) // now t is deduced to int, and intilaizer type is intializer_list<int>
So the only real difference between auto and template type deduction is that auto assumes that a braced initializer represents a std::initializer_list, but template type deduction doesn’t. you may start wondering why the auto has different behavior when using the closed braces in the declaration neither do I- if you found any conventional explanation put the link in the comment section. this was the case till the C++14 come to space- you think all your problems are solved when a newer version is realized then body you have no idea how the C++ community works. But the rule is the rule, and this means you must remember that if you declare a variable using auto and you initialize it with a braced initializer, the deduced type will always be std::initializer_list. It’s especially important to bear this in mind if you embrace the philosophy of uniform initialization of enclosing initializing values in braces as a matter of course. A classic mistake in C++11 programming is accidentally declaring a std::initializer_list variable when you mean to declare something else. This pitfall is one of the reasons some developers put braces around their initializers only when they have to.
auto createInitList() {
return { 1, 2, 3 }; // error: can't deduce type
}
the same error when using lambda in C++14:
std::vector<int>v;
auto reset_Vec = [&v](const auto& newValue) { v = newValue; }; // C++14
reset_Vec({1,2,3}); // error can't deduce type.
Things to Remember
auto type deduction is usually the same as template type deduction, but auto type deduction assumes that a braced initializer represents a std::initial izer_list, and template type deduction doesn’t.
auto in a function return type or a lambda parameter implies template type deduction, not auto type deduction.
pleas share, comment and press like if you liked the article, and wait for more on the series of Advanced Modern C++
The article is inspired from
Effective Modern C++ book
Subscribe to my newsletter
Read articles from Amr Shams directly inside your inbox. Subscribe to the newsletter, and don't miss out.
Written by
Amr Shams
Amr Shams
I am a problem solving, in love with C++, python, and Operating systems.