Automatic type deduction with the auto keyword
Since the introduction of the auto
keyword in C++11, there has been a lot of confusion in the C++ community about how to use the different flavors of auto
, such as const
auto&
, auto&
, auto&&
, and decltype(auto)
.
Using auto in function signatures
Although discouraged by some C++ programmers, in my experience the use of auto
in function signatures can increase readability when browsing and viewing header files.
Here is how the auto
syntax looks compared to the traditional syntax with explicit types:
Traditional syntax with explicit type: |
New syntax with auto: |
|
|
The auto
syntax can be used both with and without a trailing return type. The trailing return is necessary in some contexts. For example, if we are writing a virtual function, or the function declaration is put in a header file and the function definition is in a .cpp
file.
Note that the auto
syntax can also be used with free functions:
Return type |
Syntactic variants (a, b, and c correspond to the same result): |
Value |
|
Const reference |
|
Mutable reference |
|
Forwarding the return type using decltype(auto)
There is a somewhat rare version of automatic type deduction called decltype(auto)
. Its most common use is for forwarding the exact type from a function. Imagine that we are writing wrapper functions for val()
and mref()
declared in the previous table, like this:
int val_wrapper() { return val(); } // Returns int
int& mref_wrapper() { return mref(); } // Returns int&
Now, if we wanted to use return type deduction for the wrapper functions, the auto
keyword would deduce the return type to an int
in both cases:
auto val_wrapper() { return val(); } // Returns int
auto mref_wrapper() { return mref(); } // Also returns int
If we wanted our mref_wrapper()
to return an int&
, we would need to write auto&
. In this example, this would be fine, since we know the return type of mref()
. However, that's not always the case. So if we want the compiler to instead choose the exact same type without explicitly saying int&
or auto&
for mref_wrapper()
, we can use decltype(auto)
:
decltype(auto) val_wrapper() { return val(); } // Returns int
decltype(auto) mref_wrapper() { return mref(); } // Returns int&
In this way, we can avoid explicitly choosing between writing auto
or auto&
when we don't know what the function val()
or mref()
return. This is a scenario that usually happens in generic code where the type of the function that is being wrapped is a template parameter.
Using auto for variables
The introduction of the auto
keyword in C++11 has initiated quite a debate among C++ programmers. Many people think it reduces readability, or even that it makes C++ similar to a dynamically typed language. I tend to not participate in those debates, but my personal opinion is that you should (almost) always use auto
as, in my experience, it makes the code safer and less littered with clutter.
Overusing auto
can make the code harder to understand. When reading code, we usually want to know which operations are supported by some object. A good IDE can provide us with this information, but it's not explicitly there in the source code. C++20 concepts address this issue by focusing on the behavior of an object. See Chapter 8, Compile-Time Programming, for more information about C++ concepts.
I prefer to use auto
for local variables using the left-to-right initialization style. This means keeping the variable on the left, followed by an equals sign, and then the type on the right side, like this:
auto i = 0;
auto x = Foo{};
auto y = create_object();
auto z = std::mutex{}; // OK since C++17
With guaranteed copy elision introduced in C++17, the statement auto x = Foo{}
is identical to Foo x{}
; that is, the language guarantees that there is no temporary object that needs to be moved or copied in this case. This means that we can now use the left-to-right initialization style without worrying about performance and we can also use it for non-movable/non-copyable types, such as std::atomic
or std::mutex
.
One big advantage of using auto
for variables is that you will never leave a variable uninitialized since auto x;
doesn't compile. Uninitialized variables are a particularly common source of undefined behavior that you can completely eliminate by following the style suggested here.
Using auto
will help you with using the correct type for your variables. What you still need to do, though, is to express how you intend to use a variable by specifying whether you need a reference or a copy, and whether you want to modify the variable or just read from it.
A const reference
A const
reference, denoted by const auto&
, has the ability to bind to anything. The original object can never be mutated through such a reference. I believe that the const
reference should be the default choice for objects that are potentially expensive to copy.
If the const
reference is bound to a temporary object, the lifetime of the temporary will be extended to the lifetime of the reference. This is demonstrated in the following example:
void some_func(const std::string& a, const std::string& b) {
const auto& str = a + b; // a + b returns a temporary
// ...
} // str goes out of scope, temporary will be destroyed
It's also possible to end up with a const
reference by using auto&
. This can be seen in the following example:
auto foo = Foo{};
auto& cref = foo.cref(); // cref is a const reference
auto& mref = foo.mref(); // mref is a mutable reference
Even though this is perfectly valid, it is preferable to always explicitly express that we are dealing with const
references by using const auto&
, and, more importantly, we should use auto&
to only denote mutable references.
A mutable reference
In contrast to a const
reference, a mutable reference cannot bind to a temporary. As mentioned, we use auto&
to denote mutable references. Use a mutable reference only when you intend to change the object it references.
A forwarding reference
auto&&
is called a forwarding reference (also referred to as a universal reference). It can bind to anything, which makes it useful for certain cases. Forwarding references will, just like const
references, extend the lifetime of a temporary. But in contrast to the const
reference, auto&&
allows us to mutate objects it references, temporaries included.
Use auto&&
for variables that you only forward to some other code. In those forwarding cases, you rarely care about whether the variable is a const
or a mutable; you just want to pass it to some code that is actually going to use the variable.
It's important to note that auto&&
and T&&
are only forwarding references if used in a function template where T
is a template parameter of that function template. Using the &&
syntax with an explicit type, for example std::string&&
, denotes an rvalue reference and does not have the properties of a forwarding reference (rvalues and move semantics will be discussed later in this chapter).
Practices for ease of use
Although this is my personal opinion, I recommend using const auto
for fundamental types (int
, float
, and so on) and small non-fundamental types like std::pair
and std::complex
. For bigger types that are potentially expensive to copy, use const auto&
. This should cover the majority of the variable declarations in a C++ code base.
auto&
and auto
should only be used when you require the behavior of a mutable reference or an explicit copy; this communicates to the reader of the code that those variables are important as they either copy an object or mutate a referenced object. Finally, use auto&&
for forwarding code only.
Following these rules makes your code base easier to read, debug, and reason about.
It might seem odd that while I recommend using const auto
and const auto&
for most variable declarations, I tend to use a simple auto
in some places in this book. The reason for using plain auto
is the limited space that the format of a book provides.
Before moving on, we will spend a little time talking about const
and how to propagate const
when using pointers.
Const propagation for pointers
By using the keyword const
, we can inform the compiler about which objects are immutable. The compiler can then check that we don't try to mutate objects that aren't intended to be changed. In other words, the compiler checks our code for const
-correctness. A common mistake when writing const
-correct code in C++ is that a const
-initialized object can still manipulate the values that member pointers point at. The following example illustrates the problem:
class Foo {
public:
Foo(int* ptr) : ptr_{ptr} {}
auto set_ptr_val(int v) const {
*ptr_ = v; // Compiles despite function being declared const!
}
private:
int* ptr_{};
};
int main() {
auto i = 0;
const auto foo = Foo{&i};
foo.set_ptr_val(42);
}
Although the function set_ptr_val()
is mutating the int
value, it's valid to declare it const
since the pointer ptr_
itself is not mutated, only the int
object that the pointer is pointing at.
In order to prevent this in a readable way, a wrapper called std::experimental::propagate_const
has been added to the standard library extensions (included in, at the time of writing, the latest versions of Clang and GCC). Using propagate_const
, the function set_ptr_val()
will not compile. Note that propagate_const
only applies to pointers, and pointer-like classes such as std::shared_ptr
and std::unique_ptr
, but not std::function
.
The following example demonstrates how propagate_const
can be used to generate compilation errors when trying to mutate an object inside a const
function:
#include <experimental/propagate_const>
class Foo {
public:
Foo(int* ptr) : ptr_{ptr} {}
auto set_ptr(int* p) const {
ptr_ = p; // Will not compile, as expected
}
auto set_val(int v) const {
val_ = v; // Will not compile, as expected
}
auto set_ptr_val(int v) const {
*ptr_ = v; // Will not compile, const is propagated
}
private:
std::experimental::propagate_const<int*> ptr_ = nullptr;
int val_{};
};
The importance of proper use of const
in large code bases cannot be overstated, and the introduction of propagate_const
makes const
-correctness even more effective.
Next, we will have a look at move semantics and some important rules for handling resources inside a class.