Search icon CANCEL
Arrow left icon
Explore Products
Best Sellers
New Releases
Books
Videos
Audiobooks
Learning Hub
Conferences
Free Learning
Arrow right icon
Arrow up icon
GO TO TOP
Modern C++ Programming Cookbook

You're reading from   Modern C++ Programming Cookbook Recipes to explore data structure, multithreading, and networking in C++17

Arrow left icon
Product type Paperback
Published in May 2017
Publisher Packt
ISBN-13 9781786465184
Length 590 pages
Edition 1st Edition
Languages
Tools
Arrow right icon
Author (1):
Arrow left icon
Marius Bancila Marius Bancila
Author Profile Icon Marius Bancila
Marius Bancila
Arrow right icon
View More author details
Toc

Table of Contents (13) Chapters Close

Preface 1. Learning Modern Core Language Features FREE CHAPTER 2. Working with Numbers and Strings 3. Exploring Functions 4. Preprocessor and Compilation 5. Standard Library Containers, Algorithms, and Iterators 6. General Purpose Utilities 7. Working with Files and Streams 8. Leveraging Threading and Concurrency 9. Robustness and Performance 10. Implementing Patterns and Idioms 11. Exploring Testing Frameworks 12. Bibliography

Using auto whenever possible

Automatic type deduction is one of the most important and widely used features in modern C++. The new C++ standards have made it possible to use auto as a placeholder for types in various contexts and let the compiler deduce the actual type. In C++11, auto can be used for declaring local variables and for the return type of a function with a trailing return type. In C++14, auto can be used for the return type of a function without specifying a trailing type and for parameter declarations in lambda expressions. Future standard versions are likely to expand the use of auto to even more cases. The use of auto in these contexts has several important benefits. Developers should be aware of them, and prefer auto whenever possible. An actual term was coined for this by Andrei Alexandrescu and promoted by Herb Sutter--almost always auto (AAA).

How to do it...

Consider using auto as a placeholder for the actual type in the following situations:

  • To declare local variables with the form auto name = expression when you do not want to commit to a specific type:
        auto i = 42;          // int 
auto d = 42.5; // double
auto s = "text"; // char const *
auto v = { 1, 2, 3 }; // std::initializer_list<int>
  • To declare local variables with the auto name = type-id { expression } form when you need to commit to a specific type:
        auto b  = new char[10]{ 0 };            // char* 
auto s1 = std::string {"text"}; // std::string
auto v1 = std::vector<int> { 1, 2, 3 }; // std::vector<int>
auto p = std::make_shared<int>(42); // std::shared_ptr<int>
  • To declare named lambda functions, with the form auto name = lambda-expression, unless the lambda needs to be passed or return to a function:
        auto upper = [](char const c) {return toupper(c); };
  • To declare lambda parameters and return values:
        auto add = [](auto const a, auto const b) {return a + b;};
  • To declare function return type when you don't want to commit to a specific type:
        template <typename F, typename T> 
auto apply(F&& f, T value)
{
return f(value);
}

How it works...

The auto specifier is basically a placeholder for an actual type. When using auto, the compiler deduces the actual type from the following instances:

  • From the type of the expression used to initialize a variable, when auto is used to declare variables.
  • From the trailing return type or the type of the return expression of a function, when auto is used as a placeholder for the return type of a function.

In some cases, it is necessary to commit to a specific type. For instance, in the preceding example, the compiler deduces the type of s to be char const *. If the intention was to have a std::string, then the type must be specified explicitly. Similarly, the type of v was deduced as std::initializer_list<int>. However, the intention could be to have a std::vector<int>. In such cases, the type must be specified explicitly on the right side of the assignment.

There are some important benefits of using the auto specifier instead of actual types; the following is a list of, perhaps, the most important ones:

  • It is not possible to leave a variable uninitialized. This is a common mistake that developers do when declaring variables specifying the actual type, but it is not possible with auto that requires an initialization of the variable in order to deduce the type.
  • Using auto ensures that you always use the correct type and that implicit conversion will not occur. Consider the following example where we retrieve the size of a vector to a local variable. In the first case, the type of the variable is int, though the size() method returns size_t. That means an implicit conversion from size_t to int will occur. However, using auto for the type will deduce the correct type, that is, size_t:
        auto v = std::vector<int>{ 1, 2, 3 }; 
int size1 = v.size();
// implicit conversion, possible loss of data
auto size2 = v.size();
auto size3 = int{ v.size() }; // error, narrowing conversion
  • Using auto promotes good object-oriented practices, such as preferring interfaces over implementations. The lesser the number of types specified the more generic the code is and more open to future changes, which is a fundamental principle of object-oriented programming.
  • It means less typing and less concern for actual types that we don't care about anyways. It is very often that even though we explicitly specify the type, we don't actually care about it. A very common case is with iterators, but one can think of many more. When you want to iterate over a range, you don't care about the actual type of the iterator. You are only interested in the iterator itself; so, using auto saves time used for typing possibly long names and helps you focus on actual code and not type names. In the following example, in the first for loop, we explicitly use the type of the iterator. It is a lot of text to type, the long statements can actually make the code less readable, and you also need to know the type name that you actually don't care about. The second loop with the auto specifier looks simpler and saves you from typing and caring about actual types.
        std::map<int, std::string> m; 
for (std::map<int,std::string>::const_iterator it = m.cbegin();
it != m.cend(); ++it)
{ /*...*/ }

for (auto it = m.cbegin(); it != m.cend(); ++it)
{ /*...*/ }
  • Declaring variables with auto provides a consistent coding style with the type always in the right-hand side. If you allocate objects dynamically, you need to write the type both on the left and right side of the assignment, for example, int p = new int(42). With auto, the type is specified only once on the right side.

However, there are some gotchas when using auto:

  • The auto specifier is only a placeholder for the type, not for the const/volatile and references specifiers. If you need a const/volatile and/or reference type, then you need to specify them explicitly. In the following example, foo.get() returns a reference to int; when variable x is initialized from the return value, the type deduced by the compiler is int, and not int&. Therefore, any change to x will not propagate to foo.x_. In order to do so, one should use auto&:
        class foo { 
int x_;
public:
foo(int const x = 0) :x_{ x } {}
int& get() { return x_; }
};

foo f(42);
auto x = f.get();
x = 100;
std::cout << f.get() << std::endl; // prints 42
  • It is not possible to use auto for types that are not moveable:
        auto ai = std::atomic<int>(42); // error
  • It is not possible to use auto for multi-word types, such as long long, long double, or struct foo. However, in the first case, the possible workarounds are to use literals or type aliases; as for the second, using struct/class in that form is only supported in C++ for C compatibility and should be avoided anyways:
        auto l1 = long long{ 42 }; // error 
auto l2 = llong{ 42 }; // OK
auto l3 = 42LL; // OK
  • If you use the auto specifier but still need to know the type, you can do so in any IDE by putting the cursor over a variable, for instance. If you leave the IDE, however, that is not possible anymore, and the only way to know the actual type is to deduce it yourself from the initialization expression, which could probably mean searching through the code for function return types.

The auto can be used to specify the return type from a function. In C++11, this requires a trailing return type in the function declaration. In C++14, this has been relaxed, and the type of the return value is deduced by the compiler from the return expression. If there are multiple return values they should have the same type:

    // C++11 
auto func1(int const i) -> int
{ return 2*i; }

// C++14
auto func2(int const i)
{ return 2*i; }

As mentioned earlier, auto does not retain const/volatile and reference qualifiers. This leads to problems with auto as a placeholder for the return type from a function. To explain this, let us consider the preceding example with foo.get(). This time we have a wrapper function called proxy_get() that takes a reference to a foo, calls get(), and returns the value returned by get(), which is an int&. However, the compiler will deduce the return type of proxy_get() as being int, not int&. Trying to assign that value to an int& fails with an error:

    class foo 
{
int x_;
public:
foo(int const x = 0) :x_{ x } {}
int& get() { return x_; }
};

auto proxy_get(foo& f) { return f.get(); }

auto f = foo{ 42 };
auto& x = proxy_get(f); // cannot convert from 'int' to 'int &'

To fix this, we need to actually return auto&. However, this is a problem with templates and perfect forwarding the return type without knowing whether that is a value or a reference. The solution to this problem in C++14 is decltype(auto) that will correctly deduce the type:

    decltype(auto) proxy_get(foo& f) { return f.get(); } 
auto f = foo{ 42 };
decltype(auto) x = proxy_get(f);

The last important case where auto can be used is with lambdas. As of C++14, both lambda return type and lambda parameter types can be auto. Such a lambda is called a generic lambda because the closure type defined by the lambda has a templated call operator. The following shows a generic lambda that takes two auto parameters and returns the result of applying operator+ on the actual types:

    auto ladd = [] (auto const a, auto const b) { return a + b; }; 
struct
{
template<typename T, typename U>
auto operator () (T const a, U const b) const { return a+b; }
} L;

This lambda can be used to add anything for which the operator+ is defined. In the following example, we use the lambda to add two integers and to concatenate to std::string objects (using the C++14 user-defined literal operator ""s):

    auto i = ladd(40, 2);            // 42 
auto s = ladd("forty"s, "two"s); // "fortytwo"s

See also

  • Creating type aliases and alias templates
  • Understanding uniform initialization
You have been reading a chapter from
Modern C++ Programming Cookbook
Published in: May 2017
Publisher: Packt
ISBN-13: 9781786465184
Register for a free Packt account to unlock a world of extra content!
A free Packt account unlocks extra newsletters, articles, discounted offers, and much more. Start advancing your knowledge today.
Unlock this book and the full library FREE for 7 days
Get unlimited access to 7000+ expert-authored eBooks and videos courses covering every tech area you can think of
Renews at €18.99/month. Cancel anytime