Understanding auto type inference

Keywords: Programming Lambda

Last post The template type inference is described in this paper. We know that the principle of auto is based on the template type inference. Review the template type inference:


template <typename T>
void f(ParamType param);

Use the following function calls:

f(expr);

We see that template type inference involves template, function f and parameters (including template parameters and function parameters). When calling f, the compiler infers the type of T and ParamType. There is a corresponding relationship between the realization of auto and the three parts. When using auto to declare a variable, the auto keyword plays the role of T in template type inference, while the type descriptor plays the role of ParamType. Look at the following examples:

auto x = 27;  //The type descriptor is auto itself
const auto cx =x; //The type descriptor is const auto
const auto& rx =x;//The type descriptor is const auto&

The compiler uses auto to infer the above types as if it used the following template type inference:

template<typename T> 
void func_for_x(T param); //ParamType is neither a reference nor a pointer
func_for_x(27); // Infer the type of x, T is int, ParamType is int

template<typename T> 
void func_for_cx(const T param); //ParamType is neither a reference nor a pointer
func_for_cx(x); //To infer the type of cx, T is int, ParamType is const int

template<typename T> 
void func_for_rx(const T& param);//ParamType as a reference
func_for_rx(x); // Types used to infer rx

Continue to review Last post Based on the three forms of ParamType, template type inference corresponds to three different situations. The type descriptor of auto acts as ParamType, so there are three situations when using auto to declare variables:

  • A type descriptor is a pointer or reference type, but not a universal reference
  • The type descriptor is universal reference.
  • Type descriptors are neither pointers nor references.

The examples given above are the first and third cases:

auto x = 27; //Case 3x type is inferred as int
const auto cx = x; //Case 3 CX is inferred as constint
const auto &rx = x; //case 1 rx is inferred as const int&

Take an example of case 2:

auto&& uref1 = x; //x is a left value, uref1 is inferred as a left value reference
auto&& uref2 = cx; // CX const int left value, uref2 inferred as const int&
auto&& uref3 = 27; // 27 is the right value of int, uref3 is inferred as int&&

In the previous post, the non-reference ParamType in the template is degraded to a pointer when an incoming function or an array argument is passed in (while in the case of a reference to ParamType, the array argument is deduced to be a reference to an array), and so is the auto type deduction:

const char name[] =  "R. N. Briggs";
auto arr1 = name; // The type of arr1 is const char*
auto& arr2 = name; // The type of arr2 is const char (&) [13]
void someFunc(int, double); 
auto func1 = someFunc; // The type of func1 is void (*)(int, double)
auto& func2 = someFunc; // The type of func2 is void (&) (int, double)

All of the above mentioned parts are the same parts of auto and template type inference, but the following ones are different.

There are two ways to initialize an Int in C++98:

int x1=27;
int x1(27);

In C++11, uniform initialization is supported:


int x3 = {27};
int x3{27};

There is only one result of the four grammatical forms, and an Int value of 27 is initialized. Here we will all use auto to initialize:

 auto x1 = 27;
 auto x2(27);
 auto x3 = {27};
 auto x4{27}; 

The above four sentences can be compiled and passed, but they are not completely consistent with the original four forms of meaning. The first two are the same, and the last two sentences declare the variable type std::initializer_list, which contains a single element with a value of 27.

 auto x1 = 27; //x1 is int with a value of 27
 auto x2(27);//Ditto
 auto x3 = {27};//x3 is std:: initializer_list < int > with a value of {27}
 auto x4{27}; //Ditto
 

A special type inference rule for auto is used here: when auto variables are initialized with bracketed values (called unified initializers), the type of variable is inferred to std::initializer_list. If it is not possible to infer this type (for example, values in braces are not of the same type), compilation errors occur:

 auto x5 = { 1, 2, 3.0 }; // error! Types are inconsistent and cannot be inferred as std:: initializer_list<T>
 

There are two types of inference. One is to infer the unified initialization formula to std::initializer_list, and std::initializer_list itself is a template of type T. Therefore, the template type inference will be made according to the parameters in the unified initialization formula. This is the second type inference. The above type inference fails because the second type inference fails.

The only difference between auto and template type inference is the inconsistency in the processing of unified initialization. Initialization of auto variables using a unified initializer infers them to std::initializer_list, but template type inference does not:

auto x = { 11, 23, 9 }; // The type of x is std:: initializer_list<int>

template<typename T> // Template type inference equivalent to auto x
void f(T param); 

f({ 11, 23, 9 }); // Wrong! The type of T cannot be inferred here.

If you want to achieve the effect of auto, you have to follow the following ways:

template<typename T>
void f(std::initializer_list<T> initList);
f({ 11, 23, 9 }); // T is inferred as int, and the type of initList is std:: initializer_list<int>

When using auto in C++11, it's easy to make mistakes. You want to declare other variables, but you end up declaring them as std::initializer_list. Therefore, uniform initialization should be used with caution.

In C++14, auto is allowed to be used as the return value of a function, and it can also be used to modify the parameters in lambda expressions. However, these autos use template type inference rather than auto type inference, so when a function returns to auto type, returning the value of the unified initializer will make a mistake:

auto createInitList()
{
    return { 1, 2, 3 }; // Wrong! It cannot be inferred that {1,2,3}
}

The following is correct:

std::initializer_list<int> createInitList()
{
    return { 1, 2, 3 }; // 
}

In conclusion:

  • Template type inference is the basis of auto. The auto keyword acts as T in template type inference, while the type descriptor acts as ParamType.
  • For template type inference and auto type inference, inference rules are common in most scenarios, and there is a special case, that is, unified initialization.
  • In C++14, auto can be used as a function return value or as a parameter modifier of lambda expression. However, it should be noted that auto here uses template type inference rather than auto type inference.

Posted by willythemax on Sun, 05 May 2019 15:16:38 -0700