Introduction
This weekend, I tried to solve a fun puzzle - the C++ auto type deduction gauntlet and thought of creating a quick cheatsheet on auto type deduction rules. I encourage you to give it a shot.
auto is a placeholder type that gets replaced typically by deduction from an initializer.
Whenever you have:
you have three general cases:
auto x: The object being assigned is allowed to decay. We strip the RHSexpressionof all CV-qualifiers such asconstand reference modifiers&to deduce the by-value type ofx.auto& xorauto&& x: It preserves references and CV-qualifiers.const auto& x: We specify the qualifiers,autosimply deduces the base type.
There is special treatment for expressions that are functions or arrays:
- When initializing a reference(
auto&), array/function types are preserved. - Otherwise, it decays to a pointer before type deduction.
Exercises to flex your understanding of auto deduction rules
Let’s determine for each of the problems:
- The deduced type
- The value category
- What CV qualifiers are applicable
As the author of the puzzles states, let’s note that these are meant to test your understanding and some examples won’t compile.
Basics
This is fairly straight-forward. The deduced type is int. The value category of v is lvalue.
Floating-point values default to the larger double, rather than float. The value category of v is lvalue.
The deduced type is int and the value category of v is lvalue.
The u suffix is used for unsigned integer literals and the z suffix is used for the signed version of std::size_t. The suffix uz deduces to std::size_t.
The type size_t is an implementation-defined unsigned integer type that is large enough to contain the size in bytes of any object.
I didn’t get this puzzle. But, this will fail to compile. All types in an expression defined with auto have be the same.
& is the address-of operator. So, the type of v is deduced as pointer-to-int int*. The value category of v is lvalue.
x is an array of \(5\) ints, its type is int [5]. C-style arrays decay to a pointer. So, the type of v is int*. v is an lvalue.
nullptr is a value of type std::nullptr_t.
\(1\), \(2\) and \(3\) are ints. So, the type of v is deduced as std::initializer_list<int>. If the curly-braced initializer list were {1, 2.5, 4}, type deduction would fail, because we require all types in an initializer list to be the same.
Curly braced direct initialization only works with a single scalar value e.g. auto v{1}.
The type of x is deduced as std::initializer_list<int>.
To summarize:
auto+ copy list initialization : If all elements are of typeT,autois deduced asstd::initializer_list<T>.auto+ direct list initialization :- \(1\) element in braces \(\implies\)
autodeduces the type of element by-value. - \(>1\) element \(\implies\) error(ill-formed).
- \(1\) element in braces \(\implies\)
v is deduced as a function pointer, int (*) int. According to the C++ standard, a function is an object that occupies memory storage and has a lifetime. Hence, foo is an lvalue expression.
Intermediate
We now explore how references and CV-qualifiers are handled.
We strip all modifiers off x, so that results in an int. Thus, auto is deduced as int. auto always drops top-level CV qualifiers.
I didn’t quite get this one. It turns out that CV qualifiers applied to pointed-to. So, type of x is deduced as volatile const int*. The next code snip also shows the same:
#include <cassert>
#include <type_traits>
int main(){
const int& x{42};
auto& y = x;
static_assert(std::is_same_v<decltype(y), const int&>);
return 0;
}y is an lvalue reference to x. The type of y decays to int.
The type of v is deduced as int&. v is an lvalue reference.
auto is deduced as int [5]. The type of v is a reference to an array of \(5\) ints.
Functions are lvalues and the function name is a pointer to function object in memory. CV-qualifiers on parameters are thrown away during function resolution. Hence, auto is deduced as int (*)(int).
Advanced
auto&& v is a forwarding reference. x is an lvalue and lvalues bind to lvalue references. Here, we get an lvalue reference.
The return value of the function call is a prvalue. It will bind to an rvalue reference. So, the type of v is deduced as int&&.
The result of the function call y() is an lvalue, so the type of v is deduced to be int&.
The call to the constructor Foo{} creates a temporary Foo instance - a prvalue. Hence, the type of v is deduced as Foo&&.
void f(auto& param);
int x{22};
const int cx = x;
const int& rx = x;
f(x); // [1]
f(cx); // [2]
f(rx); // [3]auto& preserves references and CV-qualifiers. In the function call f(x), the type of param is deduced as int&. In the function call f(cx), the type of param is deduced as const int&. In the function call f(rx), the type of param is deduced as const int&.
Solving these auto type deduction puzzles prompted me to go watch Scott Meyer’s talk on type deduction from CppCon 2014. At 25:10, he says:
It is important to distinguish between an expression that is
const, and an expression that containsconst.
The most common place this issue occurs, is when we are dealing with pointers. Observe the following code snip:
int* const cpi;
auto p1 = cpi;
const int* pci;
auto p2 = pci;
const int* const cpci;
auto p3 = cpci;You can have a constant pointer-to-int, in which case the expression is a constant. Or you can have a pointer-to-const, in which case you have a expression containing const. And you can have a constant pointer to const, which is a constant expression that contains a const.
When deducing auto, we are deducing by-value. The top-level const/volatile is dropped. Applying this rule, we have the following:
- The type of
p1is deduced asint*. - The type of
p2is deduced asconst int*. - The type of
p3is deduced asconst int*.
THe fact that p3 is const is ignored. The fact that p3 contains const is not ignored.
The expression &pcx contains const. So, the type of ppcx is deduced as const int**.
References
- Type deduction and why you care by Scott Meyers.
decltype(auto): An overview of How, Why and Where by Jason Turner.