The Items in this book are guidelines, not rules, because guidelines have exceptions. The most important part of each Item is not the advice it offers, but the rationale behind the advice. Once you've read that, you'll be in a position to determine whether the circumstances of your project justify a violation of the Item's guidance. The true goal of this book isn't to tell you what to do or what to avoid doing, but to convey a deeper understanding of how things work in C++11 and C++14.
Terminology and Conventions
To make sure we understand one another, it's important to agree on some terminology, beginning, ironically, with “C++.” There have been four official versions of C++, each named after the year in which the corresponding ISO Standard was adopted: C++98 , C++03 , C++11 , and C++14 . C++98 and C++03 differ only in technical details, so in this book, I refer to both as C++98. When I refer to C++11, I mean both C++11 and C++14, because C++14 is effectively a superset of C++11. When I write C++14, I mean specifically C++14. And if I simply mention C++, I'm making a broad statement that pertains to all language versions.
Term I Use |
Language Versions I Mean |
C++ |
All |
C++98 |
C++98 and C++03 |
C++11 |
C++11 and C++14 |
C++14 |
C++14 |
As a result, I might say that C++ places a premium on efficiency (true for all versions), that C++98 lacks support for concurrency (true only for C++98 and C++03), that C++11 supports lambda expressions (true for C++11 and C++14), and that C++14 offers generalized function return type deduction (true for C++14 only).
C++11's most pervasive feature is probably move semantics, and the foundation of move semantics is distinguishing expressions that are rvalues from those that are lvalues . That's because rvalues indicate objects eligible for move operations, while lvalues generally don't. In concept (though not always in practice), rvalues correspond to temporary objects returned from functions, while lvalues correspond to objects you can refer to, either by name or by following a pointer or lvalue reference.
A useful heuristic to determine whether an expression is an lvalue is to ask if you can take its address. If you can, it typically is. If you can't, it's usually an rvalue. A nice feature of this heuristic is that it helps you remember that the type of an expression is independent of whether the expression is an lvalue or an rvalue. That is, given a type T
, you can have lvalues of type T as well as rvalues of type T
. It's especially important to remember this when dealing with a parameter of rvalue reference type, because the parameter itself is an lvalue:
class Widget {
public:
Widget( Widget&& rhs); // rhs is an lvalue , though it has
… // an rvalue reference type
};
Here, it'd be perfectly valid to take rhs
's address inside Widget
's move constructor, so rhs
is an lvalue, even though its type is an rvalue reference. (By similar reasoning, all parameters are lvalues.)
That code snippet demonstrates several conventions I normally follow:
• The class name is Widget
. I use Widget
whenever I want to refer to an arbitrary user-defined type. Unless I need to show specific details of the class, I use Widget
without declaring it.
• I use the parameter name rhs
(“right-hand side”). It's my preferred parameter name for the move operations (i.e., move constructor and move assignment operator) and the copy operations (i.e., copy constructor and copy assignment operator). I also employ it for the right-hand parameter of binary operators:
Matrix operator+(const Matrix& lhs, const Matrix& rhs);
It's no surprise, I hope, that lhs
stands for “left-hand side.”
• I apply special formatting to parts of code or parts of comments to draw your attention to them. In the Widget
move constructor above, I've highlighted the declaration of rhs
and the part of the comment noting that rhs
is an lvalue. Highlighted code is neither inherently good nor inherently bad. It's simply code you should pay particular attention to.
• I use “ …
” to indicate “other code could go here.” This narrow ellipsis is different from the wide ellipsis (“ ...
”) that's used in the source code for C++11's variadic templates. That sounds confusing, but it's not. For example:
template // these are C++
void processVals(const Ts&... params) // source code
{ // ellipses
… // this means "some
// code goes here"
}
The declaration of processVals
shows that I use typename
when declaring type parameters in templates, but that's merely a personal preference; the keyword class
would work just as well. On those occasions where I show code excerpts from a C++ Standard, I declare type parameters using class, because that's what the Standards do.
When an object is initialized with another object of the same type, the new object is said to be a copy of the initializing object, even if the copy was created via the move constructor. Regrettably, there's no terminology in C++ that distinguishes between an object that's a copy-constructed copy and one that's a move-constructed copy:
void someFunc(Widget w); // someFunc's parameter w
// is passed by value
Widget wid; // wid is some Widget
someFunc(wid); // in this call to someFunc,
// w is a copy of wid that's
// created via copy construction
someFunc(std::move(wid)); // in this call to SomeFunc,
// w is a copy of wid that's
// created via move construction
Copies of rvalues are generally move constructed, while copies of lvalues are usually copy constructed. An implication is that if you know only that an object is a copy of another object, it's not possible to say how expensive it was to construct the copy. In the code above, for example, there's no way to say how expensive it is to create the parameter w without knowing whether rvalues or lvalues are passed to someFunc
. (You'd also have to know the cost of moving and copying Widget
s.)
In a function call, the expressions passed at the call site are the function's arguments . The arguments are used to initialize the function's parameters . In the first call to someFunc
above, the argument is wid
. In the second call, the argument is std::move(wid)
. In both calls, the parameter is w. The distinction between arguments and parameters is important, because parameters are lvalues, but the arguments with which they are initialized may be rvalues or lvalues. This is especially relevant during the process of perfect forwarding , whereby an argument passed to a function is passed to a second function such that the original argument's rvalueness or lvalueness is preserved. (Perfect forwarding is discussed in detail in Item 30.)
Well-designed functions are exception safe , meaning they offer at least the basic exception safety guarantee (i.e., the basic guarantee ). Such functions assure callers that even if an exception is thrown, program invariants remain intact (i.e., no data structures are corrupted) and no resources are leaked. Functions offering the strong exception safety guarantee (i.e., the strong guarantee ) assure callers that if an exception arises, the state of the program remains as it was prior to the call.
Читать дальше