New Features of C++: Automatic Type Inference

c17_grammatech_autotype.jpg
 
Automatic type inference (or type deduction) is where you allow the compiler to infer the type information used for a declaration by obtaining it from some related expression. For instance, when you have an initializer for a locally declared variable, the type information for the declaration can be obtained from the type of the initializer. C++11 added automatic type inference via two mechanisms: the auto and decltype type specifiers. The auto type specifier is used when the type information can be inferred from some external input such as an initializer, and the decltype type specifier is used when the type information needs to be inferred from either some an arbitrary expression. Both are used (and from some perspectives, abused) with regularity in modern C++ code, but auto is by far the more common of the two constructs. For instance:

#include <string>
 
std::string func();

int main() {
  auto str = func();
}

In this example, the type of "str" is deduced based on the type of the expression "func()" to be std::string. You might wonder why this language feature is all that interesting, but"if you recall the post about lambdas, it was noted that lambdas have a type but that type is unique and cannot be spelled by the user. For lambdas that are stored as local variables, use of automatic type deduction is obligatory. For instance:
 
#include <iostream>
 

int main() {
  auto fn = [](const char *msg) { std::cout << msg << std::endl; };

  fn("hello");
  fn("world");
}

So why is there a second mechanism for deducing types if auto works so splendidly? Because sometimes the type cannot be fathomed through initialization because there's no place for that initialization to take place. For instance, imagine a generic function that adds two values together and returns the result.

template <typename T1, typename T2>
??? add(T1 lhs, T2 rhs) { return lhs + rhs; }

It's impossible to tell for an arbitrary T1 and T2 what the return type should be. If T1 and T2 are both int, then it's pretty easy. But what if T1 is a class type and T2 is an arithmetic type, and T1 overloads operator+()? You can see how this quickly becomes an unsolvable problem without type inference. To solve this problem, starting in C++11 you can use a trailing return type to signify that the type will be automatically deduced, and this trailing return type is exactly where decltype() shines.

template <typename T1, typename T2>
auto add(T1 lhs, T2 rhs) -> decltype(lhs + rhs) { return lhs + rhs; }

The auto type specifier signals that the return type is automatically deduced, the -> after the parameter list starts the declaration of the actual return type (hence, "trailing return type"), and decltype is used to automatically deduce what the return type should be. Note that the expression argument to decltype() is not evaluated (much like the expression argument to sizeof() isn't evaluated).

However, automatic type deduction is useful beyond inferring lambda type information and writing really ugly trailing return types. In fact, this language feature has proven so popular that it was extended in C++14 to allow for deducing the return types of functions without a trailing return type so long as all return statements in the function agree on the type of the returned values, which makes our previous example considerably less redundant:

template <typename T1, typename T2>
auto add(T1 lhs, T2 rhs) { return lhs + rhs; }

As you can imagine, such a powerful change to the type system has lead to three different strategies for when and how to use this construct: always use auto, never use auto, use auto "when appropriate". My personal preference for when to use type deduction falls into the "when appropriate" category because understanding C++ often requires knowledge of the types involved, and type deduction can hide type information. For this reason, I only use type deduction when required (lambdas, generic programming) or when it obviates the need to redundantly spell out the type, e.g., auto i = static_cast<int>(blah()); However, there are other reasonable uses of type deduction that may not match my personal preferences. As with other style-based guidance, the important thing is to pick a coding guideline and apply it uniformly. 

Automatic type inference is a powerful new feature that changes the way
we write modern C++ code. It allows developers to focus less on the spelling of type names and instead focus on expressions and side effects in the code. As with many powerful tools, it's good to be aware it exists and use it when it's the right tool for the job, but it can also be overused to the possible detriment of code readability and maintenance. In the next post I will discuss some smaller yet useful new features of C++.