intx1; // potentially uninitialized
autox2; // error! initializer required
autox3 = 0; // fine, x's value is well-defined
Said highway lacks the potholes associated with declaring a local variable whose value is that of a dereferenced iterator:
template // as before
void dwim(It b, It e) {
while (b != e) {
autocurrValue = *b;
…
}
}
And because auto
uses type deduction (see Item 2), it can represent types known only to compilers:
autoderefUPLess = // comparison func.
[](const std::unique_ptr& p1, // for Widgets
const std::unique_ptr& p2) // pointed to by
{ return *p1 < *p2; }; // std::unique_ptrs
Very cool. In C++14, the temperature drops further, because parameters to lambda expressions may involve auto
:
auto derefLess = // C++14 comparison
[](const auto& p1, // function for
const auto& p2) // values pointed
{ return *p1 < *p2; }; // to by anything
// pointer-like
Coolness notwithstanding, perhaps you're thinking we don't really need auto
to declare a variable that holds a closure, because we can use a std::function
object. It's true, we can, but possibly that's not what you were thinking. And maybe now you're thinking “What's a std::function
object?” So let's clear that up.
std::function
is a template in the C++11 Standard Library that generalizes the idea of a function pointer. Whereas function pointers can point only to functions, however, std::function
objects can refer to any callable object, i.e., to anything that can be invoked like a function. Just as you must specify the type of function to point to when you create a function pointer (i.e., the signature of the functions you want to point to), you must specify the type of function to refer to when you create a std::function
object. You do that through std::function
's template parameter. For example, to declare a std::function
object named func
that could refer to any callable object acting as if it had this signature,
bool(const std::unique_ptr&, // C++11 signature for
const std::unique_ptr&) // std::unique_ptr
// comparison function
you'd write this:
std::function&,
const std::unique_ptr&)> func;
Because lambda expressions yield callable objects, closures can be stored in std::function
objects. That means we could declare the C++11 version of derefUPLess
without using auto
as follows:
std::function&,
const std::unique_ptr&)>
derefUPLess = [](const std::unique_ptr& p1,
const std::unique_ptr& p2)
{ return *p1 < *p2; };
It's important to recognize that even setting aside the syntactic verbosity and need to repeat the parameter types, using std::function
is not the same as using auto
. An auto
-declared variable holding a closure has the same type as the closure, and as such it uses only as much memory as the closure requires. The type of a std::function
-declared variable holding a closure is an instantiation of the std::function
template, and that has a fixed size for any given signature. This size may not be adequate for the closure it's asked to store, and when that's the case, the std::function
constructor will allocate heap memory to store the closure. The result is that the std::function
object typically uses more memory than the auto
-declared object. And, thanks to implementation details that restrict inlining and yield indirect function calls, invoking a closure via a std::function
object is almost certain to be slower than calling it via an auto-declared object. In other words, the std::function
approach is generally bigger and slower than the auto
approach, and it may yield out-of-memory exceptions, too. Plus, as you can see in the examples above, writing “ auto
” is a whole lot less work than writing the type of the std::function
instantiation. In the competition between auto
and std::function
for holding a closure, it's pretty much game, set, and match for auto
. (A similar argument can be made for auto
over std::function
for holding the result of calls to std::bind
, but in Item 34, I do my best to convince you to use lambdas instead of std::bind
, anyway.)
The advantages of auto
extend beyond the avoidance of uninitialized variables, verbose variable declarations, and the ability to directly hold closures. One is the ability to avoid what I call problems related to “type shortcuts.” Here's something you've probably seen — possibly even written:
std::vector v;
…
unsignedsz = v.size();
The official return type of v.size()
is std::vector::size_type
, but few developers are aware of that. std::vector::size_type
is specified to be an unsigned integral type, so a lot of programmers figure that unsigned is good enough and write code such as the above. This can have some interesting consequences. On 32-bit Windows, for example, both unsigned
and std::vector::size_type
are the same size, but on 64-bit Windows, unsigned
is 32 bits, while std::vector::size_type
is 64 bits. This means that code that works under 32-bit Windows may behave incorrectly under 64-bit Windows, and when porting your application from 32 to 64 bits, who wants to spend time on issues like that?
Using auto
ensures that you don't have to:
autosz = v.size(); // sz's type is std::vector::size_type
Still unsure about the wisdom of using auto
? Then consider this code:
std::unordered_map m;
…
for (const std::pair& p : m) {
… // do something with p
}
This looks perfectly reasonable, but there's a problem. Do you see it?
Recognizing what's amiss requires remembering that the key part of a s td::unordered_map
is const
, so the type of std::pair
in the hash table (which is what a std::unordered_map
is) isn't std::pair
, it's std::pair < const std::string, int>
. But that's not the type declared for the variable p
in the loop above. As a result, compilers will strive to find a way to convert std::pair
objects (i.e., what's in the hash table) to s td::pair
objects (the declared type for p
). They'll succeed by creating a temporary object of the type that p wants to bind to by copying each object in m
, then binding the reference p
to that temporary object. At the end of each loop iteration, the temporary object will be destroyed. If you wrote this loop, you'd likely be surprised by this behavior, because you'd almost certainly intend to simply bind the reference p
to each element in m
.
Читать дальше