With this information in mind, look again at this part of the original code:
bool highPriority = features(w)[5]; // declare highPriority's
// type explicitly
Here, features
returns a std::vector
object, on which operator[]
is invoked. operator[]
returns a std::vector::reference
object, which is then implicitly converted to the bool
that is needed to initialize highPriority
. highPriority
thus ends up with the value of bit 5 in the std::vector
returned by features
, just like it's supposed to.
Contrast that with what happens in the auto
-ized declaration for highPriority
:
autohighPriority = features(w)[5]; // deduce highPriority's
// type
Again, features
returns a std::vector
object, and, again, operator[]
is invoked on it. operator[]
continues to return a std::vector::reference
object, but now there's a change, because auto
deduces that as the type of highPriority
. highPriority
doesn't have the value of bit 5 of the std::vector
returned by features
at all.
The value it does have depends on how std::vector::reference
is implemented. One implementation is for such objects to contain a pointer to the machine word holding the referenced bit, plus the offset into that word for that bit. Consider what that means for the initialization of highPriority
, assuming that such a std::vector::reference
implementation is in place.
The call to features
returns a temporary std::vector
object. This object has no name, but for purposes of this discussion, I'll call it temp
. operator[]
is invoked on temp
, and the std::vector::reference
it returns contains a pointer to a word in the data structure holding the bits that are managed by temp , plus the offset into that word corresponding to bit 5. highPriority
is a copy of this std::vector::reference
object, so highPriority
, too, contains a pointer to a word in temp
, plus the offset corresponding to bit 5. At the end of the statement, temp
is destroyed, because it's a temporary object. Therefore, highPriority
contains a dangling pointer, and that's the cause of the undefined behavior in the call to processWidget
:
processWidget(w, highPriority); // undefined behavior!
// highPriority contains
// dangling pointer!
std::vector::reference
is an example of a proxy class : a class that exists for the purpose of emulating and augmenting the behavior of some other type. Proxy classes are employed for a variety of purposes. std::vector::reference
exists to offer the illusion that operator[]
for std::vector
returns a reference to a bit, for example, and the Standard Library's smart pointer types (see Chapter 4) are proxy classes that graft resource management onto raw pointers. The utility of proxy classes is well-established. In fact, the design pattern “Proxy” is one of the most longstanding members of the software design patterns Pantheon.
Some proxy classes are designed to be apparent to clients. That's the case for std::shared_ptr
and std::unique_ptr
, for example. Other proxy classes are designed to act more or less invisibly. std::vector::reference
is an example of such “invisible” proxies, as is its std::bitset
compatriot, std::bitset::reference
.
Also in that camp are some classes in C++ libraries employing a technique known as expression templates . Such libraries were originally developed to improve the efficiency of numeric code. Given a class Matrix
and Matrix
objects m1
, m2
, m3
, and m4
, for example, the expression
Matrix sum = m1 + m2 + m3 + m4;
can be computed much more efficiently if operator+
for Matrix
objects returns a proxy for the result instead of the result itself. That is, operator+
for two Matrix
objects would return an object of a proxy class such as Sum
instead of a Matrix
object. As was the case with std::vector::reference
and bool
, there'd be an implicit conversion from the proxy class to Matrix
, which would permit the initialization of sum
from the proxy object produced by the expression on the right side of the “ =
”. (The type of that object would traditionally encode the entire initialization expression, i.e., be something like Sum, Matrix>, Matrix>
. That's definitely a type from which clients should be shielded.)
As a general rule, “invisible” proxy classes don't play well with auto
. Objects of such classes are often not designed to live longer than a single statement, so creating variables of those types tends to violate fundamental library design assumptions. That's the case with std::vector::reference
, and we've seen that violating that assumption can lead to undefined behavior.
You therefore want to avoid code of this form:
auto someVar = expression of "invisible" proxy class type ;
But how can you recognize when proxy objects are in use? The software employing them is unlikely to advertise their existence. They're supposed to be invisible , at least conceptually! And once you've found them, do you really have to abandon auto
and the many advantages Item 5demonstrates for it?
Let's take the how-do-you-find-them question first. Although “invisible” proxy classes are designed to fly beneath programmer radar in day-to-day use, libraries using them often document that they do so. The more you've familiarized yourself with the basic design decisions of the libraries you use, the less likely you are to be blindsided by proxy usage within those libraries.
Where documentation comes up short, header files fill the gap. It's rarely possible for source code to fully cloak proxy objects. They're typically returned from functions that clients are expected to call, so function signatures usually reflect their existence. Here's the spec for std::vector::operator[]
, for example:
namespace std { // from C++ Standards
template
class vector {
public:
…
class reference{ … };
referenceoperator[](size_type n);
…
};
}
Assuming you know that operator[]
for std::vector
normally returns a T&
, the unconventional return type for operator[]
in this case is a tip-off that a proxy class is in use. Paying careful attention to the interfaces you're using can often reveal the existence of proxy classes.
In practice, many developers discover the use of proxy classes only when they try to track down mystifying compilation problems or debug incorrect unit test results. Regardless of how you find them, once auto
has been determined to be deducing the type of a proxy class instead of the type being proxied, the solution need not involve abandoning auto
. auto
itself isn't the problem. The problem is that auto
isn't deducing the type you want it to deduce. The solution is to force a different type deduction. The way you do that is what I call the explicitly typed initializer idiom .
Читать дальше