Skip to content

A hacked up Python script to help developers explore and learn C++11's type deduction rules.

Notifications You must be signed in to change notification settings

stonea/C-Type-Deduction-Explorer

Folders and files

NameName
Last commit message
Last commit date

Latest commit

 

History

25 Commits
 
 
 
 
 
 
 
 

Repository files navigation

The C++ Type Deduction Explorer

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&.

How the tool helps

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.

How to Use

  • 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.


So, let's learn some type deduction rules

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:

Template Type Deduction

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;
}

Auto Type Deduction

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 like double fcn(int, int); and passing them to the whatTheHeckAreYou 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)); }

About

A hacked up Python script to help developers explore and learn C++11's type deduction rules.

Resources

Stars

Watchers

Forks

Releases

No releases published

Packages

No packages published