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, featuresreturns a std::vectorobject, on which operator[]is invoked. operator[]returns a std::vector::referenceobject, which is then implicitly converted to the boolthat is needed to initialize highPriority. highPrioritythus ends up with the value of bit 5 in the std::vectorreturned 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, featuresreturns a std::vectorobject, and, again, operator[]is invoked on it. operator[]continues to return a std::vector::referenceobject, but now there's a change, because autodeduces that as the type of highPriority. highPrioritydoesn't have the value of bit 5 of the std::vectorreturned by featuresat all.
The value it does have depends on how std::vector::referenceis 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::referenceimplementation is in place.
The call to featuresreturns a temporary std::vectorobject. 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::referenceit 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. highPriorityis a copy of this std::vector::referenceobject, 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, highPrioritycontains 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::referenceis 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::referenceexists to offer the illusion that operator[]for std::vectorreturns 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_ptrand std::unique_ptr, for example. Other proxy classes are designed to act more or less invisibly. std::vector::referenceis an example of such “invisible” proxies, as is its std::bitsetcompatriot, 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 Matrixand Matrixobjects m1, m2, m3, and m4, for example, the expression
Matrix sum = m1 + m2 + m3 + m4;
can be computed much more efficiently if operator+for Matrixobjects returns a proxy for the result instead of the result itself. That is, operator+for two Matrixobjects would return an object of a proxy class such as Suminstead of a Matrixobject. As was the case with std::vector::referenceand bool, there'd be an implicit conversion from the proxy class to Matrix, which would permit the initialization of sumfrom 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 autoand 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::vectornormally 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 autohas been determined to be deducing the type of a proxy class instead of the type being proxied, the solution need not involve abandoning auto. autoitself isn't the problem. The problem is that autoisn'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 .
Читать дальше