The C++ Type Deduction Explorer is a utility to help programmers learn and explore how C++11 compilers deduce types. Often the most effective way to learn a new language feature is to play around with it. However, by itself C++11 doesn't make it easy to explore what variables, expressions, and template typenames are deduced to be.
Although programmers can get a string-based representation of an expression's
type by using typeid(expr).name()
, this string is often cryptic (with mangled
names) or unreliable (C++11 requires that typeid present information back as
though the expression were passed to a template). In "Effective Modern C++"
Scott Meyers presents a trick to overcome these issues. He suggests using an
undefined template to produce a compile-time error that presents type
information back to the user. The trick works, but it can be tedious to
repeatedly set up code to produce an error, run the compiler, and extract type
information from the error messages. Luckily the "Type Deduction Explorer"
tool eliminates this tedious task by automating the process for you.
To understand what the Deduction Explorer does, it helps to understand the
trick Scott Meyers Presents in "Effective Modern C++". I've recreated this
trick as the whatTheHeckAreYou
macro in the following code:
template<typename T>
class IAmA;
#define whatTheHeckAreYou(expr) \
IAmA<decltype(expr)> blah;
int main(int argc, char *argv[]) {
int x = 42;
int const & y = x;
whatTheHeckAreYou(y);
}
What the macro does is try and instantiate a template that is declared but not defined. When the compiler (in this case gcc 4.8.3) fails to instantiate the undefined template it presents the following error:
./test.cpp: In function int main(int, char**):
./test.cpp:6:26: error: aggregate IAmA<const int&> blah has incomplete type and cannot be defined
IAmA<decltype(expr)> blah;
^
./test.cpp:11:5: note: in expansion of macro whatTheHeckAreYou
whatTheHeckAreYou(y);
^
The error message reveals that y's type is, as we
expect, a const int&
. Things get more interesting when we use
whatTheHeckAreYou
in the context of a template:
template<typename T>
void foo(T&& param) {
whatTheHeckAreYou(param);
}
int main(int argc, char *argv[]) {
int x = 42;
int const & y = x;
foo(y);
}
In this context gcc presents the following error:
./test.cpp: In instantiation of void foo(T&&) [with T = const int&]:
./test.cpp:16:10: required from here
./test.cpp:6:26: error: IAmA<const int&> blah has incomplete type
IAmA<decltype(expr)> blah;
^
./test.cpp:10:5: note: in expansion of macro whatTheHeckAreYou
whatTheHeckAreYou(param);
^
This message shows that both T
and param
are inferred to be const int&
.
Briefly, this tool calls g++ with -std=c++11
and extracts error messages to
generate tables like the one below. Each row in the following table lists what
C++11 deduces what the value of T
and x
are when instantiating one of the
following templates.
int var = 1;
const int constValue = 2;
int& reference = var;
const int& constReference = var;
template<typename T>
void lval(T x) { whatTheHeckAreYou(x); }
template<typename T>
void lvalConst(T const x) { whatTheHeckAreYou(x); }
template<typename T>
void lvalRef(T& x) { whatTheHeckAreYou(x); }
template<typename T>
void lvalConstRef(T const & x) { whatTheHeckAreYou(x); }
template<typename T>
void rvalRef(T&& x) { whatTheHeckAreYou(x); }
For example, if we were to call lvalRef(constVar)
, the lvalRef
template
will be instantiated with the type of T
being int
and the type of x
(the
parameter in the template) being const int&
.
Run the script and you should get the following table:
.---------------------------------------------------------------------------, | SUBSTITUTION | type of T | type of expr | |---------------------------------------------------------------------------| | whatTheHeckAreYou( var ) | None | int | | whatTheHeckAreYou( constVar ) | None | const int | | whatTheHeckAreYou( reference ) | None | int& | | whatTheHeckAreYou( constReference ) | None | const int& | | whatTheHeckAreYou( 42 ) | None | int | | | | | | lvalRef( var ) | int | int& | | lvalRef( constVar ) | const int | const int& | | lvalRef( reference ) | int | int& | | lvalRef( constReference ) | const int | const int& | | lvalRef( 42 ) | int | * | | | | | | lvalConstRef( var ) | int | const int& | | lvalConstRef( constVar ) | int | const int& | | lvalConstRef( reference ) | int | const int& | | lvalConstRef( constReference ) | int | const int& | | lvalConstRef( 42 ) | int | const int& | | | | | | rvalRef( var ) | int& | int& | | rvalRef( constVar ) | const int& | const int& | | rvalRef( reference ) | int& | int& | | rvalRef( constReference ) | const int& | const int& | | rvalRef( 42 ) | int | int&& | '---------------------------------------------------------------------------' * This case did not match the expected pattern. Likely because you attempted to bind an lvalue reference to an rvalue that is a literal constant.
This script generates this table by substituting the value in the
"SUBSTITUTION" column, in the place marked SUBSTITUTION_POINT
in this file:
https://github.com/stonea/C-Type-Deduction-Explorer/blob/master/templateFile.cpp.
The script runs the generated file through gcc, extracts type
information presented in the error messages, and presents the results to the
user in tabular form.
- Make sure you have gcc installed. This tool is known to work with gcc 4.8.3. Since the script relies on regexp to extract information from a GCC error message it's pretty brittle so you may have to adjust the regexp to get it to work.
- To run just execute
python generator.py
- To modify what is substituted, modify the 'substitutions' list in
generator.py
.
For more details, read the comments at the top of the generator file https://github.com/stonea/C-Type-Deduction-Explorer/blob/master/generator.py.
In this section I use the Deduction Explorer tool to describe how C++11 deduces types. For a more complete description refer to Scott Meyers' "Effective Modern C++". The information here basically summarizes of the first two items of the book.
In C++11, there are three different places type deduction occurs:
- In templates
- With
auto
- With
decltype
In C++11, decltype simply resolves to the type passed to it. We'll explorer the first two of these cases individually:
Given a template: template<typename T> void foo(...param-decl...)
. There are
different forms param-decl might take:
template<typename T> void foo(T param)
(without any qualifiers)template<typename T> void foo(T* param)
(a pointer)template<typename T> void foo(T& param)
(an l-value reference)template<typename T> void foo(T&& param)
(a universal reference)
The way template type deduction works differs depending on the form param-decl takes. Specifically, whether
- (1) param-decl is not qualified as a pointer nor a reference, or if
- (2) param-decl is qualified as a pointer or reference, or if
- (3) param-decl is qualified as a universal reference (i.e. T&&).
So, recall our four templates:
template<typename T>
void lval(T x) { whatTheHeckAreYou(x); }
template<typename T>
void lvalRef(T& x) { whatTheHeckAreYou(x); }
template<typename T>
void lvalConstRef(T const & x) { whatTheHeckAreYou(x); }
template<typename T>
void rvalRef(T&& x) { whatTheHeckAreYou(x); }
and the following variables:
int var = 1;
const int constValue = 2;
int& reference = var;
const int& constReference = var;
In case 1 (Param-decl is neither a pointer nor reference) we can see that whether a variable is a
const or reference doesn't matter to how T
is deduced:
.-------------------------------------------------------------------,
| SUBSTITUTION | type of T | type of expr |
|-------------------------------------------------------------------|
| lval( var ) | int | int |
| lval( constVar ) | int | int |
| lval( reference ) | int | int |
| lval( constReference ) | int | int |
| lval( 42 ) | int | int |
| | | |
| lvalConst( var ) | int | const int |
| lvalConst( constVar ) | int | const int |
| lvalConst( reference ) | int | const int |
| lvalConst( constReference ) | int | const int |
| lvalConst( 42 ) | int | const int |
'-------------------------------------------------------------------'
And why should it? When you pass an argument to a function by value, you make a copy of it. Thus the template's parameter can be changed all it wants without modifying the actual argument.
However, in case 2 (Param-decl is to a pointer or reference type), T
strips the reference of a variable (if there), then to get the type for param
we stick a reference (or pointer) qualifier on. Notice that 'const' is sticky:
when a const is passed to the template it stays a const in the type of T
and
the type of param
. This make sense. If we passed a constant-variable to a
template by reference we would be suprised if the template were able to modify
it!
.---------------------------------------------------------------------------,
| SUBSTITUTION | type of T | type of expr |
|---------------------------------------------------------------------------|
| lvalRef( var ) | int | int& |
| lvalRef( constVar ) | const int | const int& |
| lvalRef( reference ) | int | int& |
| lvalRef( constReference ) | const int | const int& |
| lvalRef( 42 ) | int | int& |
'---------------------------------------------------------------------------'
Case 3 is a little bit less intuitive, but its critical to understand in order to understand C++11. This case is all about universal references.
Scott Meyers coined the term universal reference to describe an rvalue template reference of the form T&&: an rvalue reference without any cv qualification. See item 24 in "Effective Modern C++" for a more detailed explanation. Since the publication of Meyers' book the C++ committee has come up with their own term for this type of reference, namely forwarding references. Both terms are equivalent.
With universal references if what's passed in is an lvalue then T
becomes an
lvalue-reference, and if what's passed in is an rvalue T
becomes an rvalue
reference. We can see an rvalue reference passed to a template that takes a
universal reference, in the rvalRef( 42 )
row in the following table. Also
note that even though var does not have a reference type (it's just an int
),
T will deduce to the reference int&
.
.-----------------------------------------------------------------,
| SUBSTITUTION | type of T | type of expr |
|-----------------------------------------------------------------|
| rvalRef( var ) | int& | int& |
| rvalRef( constVar ) | const int& | const int& |
| rvalRef( reference ) | int& | int& |
| rvalRef( constReference ) | const int& | const int& |
| rvalRef( 42 ) | int | int&& |
'-----------------------------------------------------------------'
This behavior may be counter intuitive because its different than what you see for r-value references in non template functions. To get a sense of why this matters consider the following:
#include <iostream>
void foo(int&& i) {
std::cout << "Foo is passed: " << i << std::endl;
i = 42;
}
template <typename T>
void bar(T&& i) {
std::cout << "Bar is passed: " << i << std::endl;
i = 42;
}
int main(int argc, char *argv[]) {
int x = 1;
foo(2); // works fine
foo(x); // produces an error: cannot bind rvalue reference of type �int&&� to lvalue of type �int�
bar(2); // works fine
bar(x); // works fine, will mutate x
std::cout << "x is: " << x << std::endl; // x will be 42
return 1;
}
Now, to learn auto type deduction, suppose we have the following variables:
int returnInt() { return 42; }
int const returnConstInt() { return 42; }
int& returnRefToInt() { static var = 42; return var; }
int const & returnRefToConstInt() { static var = 42; return var; }
auto auto_var = var; // var is an: int
auto auto_constVar = constVar; // constVar is an: int const
auto auto_reference = reference; // reference is an: int&
auto auto_constReference = constReference; // constReference is an: int & const
auto& auto_ref_var = var; // var is an: int
auto& auto_ref_constVar = constVar; // constVar is an: int const
auto& auto_ref_reference = reference; // reference is an: int&
auto& auto_ref_constReference = constReference; // constReference is an: int & const
auto const & auto_cref_var = var; // var is an: int
auto const & auto_cref_constVar = constVar; // constVar is an: int const
auto const & auto_cref_reference = reference; // reference is an: int&
auto const & auto_cref_constReference = constReference; // constReference is an: int & const
auto&& auto_rref_var = var; // var is an: int
auto&& auto_rref_constVar = constVar; // constVar is an: int const
auto&& auto_rref_reference = reference; // reference is an: int&
auto&& auto_rref_constReference = constReference; // constReference is an: int & const
auto&& auto_rref_42 = 42;
auto&& auto_rref_rvalue_int = returnInt();
auto&& auto_rref_rvalue_cint = returnConstInt();
auto&& auto_rref_value_rint = returnRefToInt();
auto&& auto_rref_value_crint = returnRefToConstInt();
Auto type deduction works like template type deduction. For auto's that are not to a pointer, reference, or universal reference type (case 1), the deduced type will have it's 'const' and 'reference' qualifiers stripped away:
.-----------------------------------------------------------,
| SUBSTITUTION | type of expr |
|-----------------------------------------------------------|
| whatTheHeckAreYou( auto_var ) | int |
| whatTheHeckAreYou( auto_constVar ) | int |
| whatTheHeckAreYou( auto_reference ) | int |
| whatTheHeckAreYou( auto_constReference ) | int |
'-----------------------------------------------------------'
When we use auto&
or auto const&
(case 2), we can think of the
reference being removed (if there was one to begin with) and then the
qualifiers on auto being stuck on.
.----------------------------------------------------------------,
| SUBSTITUTION | type of expr |
|----------------------------------------------------------------|
| whatTheHeckAreYou( auto_ref_var ) | int& |
| whatTheHeckAreYou( auto_ref_constVar ) | const int& |
| whatTheHeckAreYou( auto_ref_reference ) | int& |
| whatTheHeckAreYou( auto_ref_constReference ) | const int& |
| | |
| whatTheHeckAreYou( auto_cref_var ) | const int& |
| whatTheHeckAreYou( auto_cref_constVar ) | const int& |
| whatTheHeckAreYou( auto_cref_reference ) | const int& |
| whatTheHeckAreYou( auto_cref_constReference ) | const int& |
'----------------------------------------------------------------'
In case 3, as with template deduction, if what's assigned to auto&& is an l-value, the resulting type is an l-value reference, and if what's assigned to auto&& is an r-value, what results is an r-value.
.----------------------------------------------------------------,
| SUBSTITUTION | type of expr |
|----------------------------------------------------------------|
| whatTheHeckAreYou( auto_rref_var ) | int& |
| whatTheHeckAreYou( auto_rref_constVar ) | const int& |
| whatTheHeckAreYou( auto_rref_reference ) | int& |
| whatTheHeckAreYou( auto_rref_constReference ) | const int& |
| whatTheHeckAreYou( auto_rref_42 ) | int&& |
| whatTheHeckAreYou( auto_rref_rvalue_int ) | int&& |
| whatTheHeckAreYou( auto_rref_rvalue_cint ) | int&& |
| whatTheHeckAreYou( auto_rref_value_rint ) | int& |
| whatTheHeckAreYou( auto_rref_value_crint ) | const int& |
'----------------------------------------------------------------'
Again, the inference of auto may be unintuitive when comparing it against other rvalue references. Consider the following example:
#include <iostream>
int main(int argc, char *argv[]) {
int x = 42;
int& y = x;
int&& rvalueRef1 = 42; // Fine, we're binding to an rvalue
//int&& rvalueRef2 = x; // Error cannot bind rvalue type int&& to lvalue int
//int&& rvalueRef3 = y; // Error cannot bind rvalue type int&& to lvalue int
auto&& autoRef1 = 42; // Fine, autoRef will be int&&
auto&& autoRef2 = x; // Fine, autoRef will be int&
auto&& autoRef3 = y; // Fine, autoRef will be int&
autoRef1 = 100; // Legal assignment, no mutation to any other variable
std::cout << "Value of x after first assignment: " << x << std::endl;
autoRef2 = 200; // Mutates x
std::cout << "Value of x after second assignment: " << x << std::endl;
autoRef3 = 300; // Mutates x
std::cout << "Value of x after third assignment: " << x << std::endl;
return 1;
}
/*
Comment out the erroring lines and the expected output is:
Value of x after first assignment: 42
Value of x after second assignment: 200
Value of x after third assignment: 300
*/
There is one way type deduction of auto differs with type deduction of templates: when things are enclosed in curly braces. For auto curly braces deduce to an initializor list, for templates the braces are an error (hence we list None in the column):
.---------------------------------------------------------------,
| SUBSTITUTION | type of expr |
|---------------------------------------------------------------|
| whatTheHeckAreYou( initList ) | std::initializer_list<int> |
| lval( {1,2,3,4,5} ) | None |
| lvalConst( {1,2,3,4,5} ) | None |
| lvalRef( {1,2,3,4,5} ) | None |
| lvalConstRef( {1,2,3,4,5} ) | None |
| rvalRef( {1,2,3,4,5} ) | None |
'---------------------------------------------------------------'
And that's it! See C++11 type deduction isn't too hairy.
Here's some ideas of additional things you can explore with the tool:
-
Get a grasp of how function and array types decay by defining an array such as
int array[10];
and declaring a function likedouble fcn(int, int);
and passing them to thewhatTheHeckAreYou
maro. -
Get a grasp on how std::move works. For example by trying
whatTheHeckAreYou(std::move( var ))
. -
Get a grasp on how std::forward works by making new functions in templateFile.cpp such as:
template<typename T>
void lval_fwd(T x) { whatTheHeckAreYou(std::forward<decltype(x)>(x)); }