B. Stroustrup - The C++ Programming Language (794319), страница 23
Текст из файла (страница 23)
Memberwise copy would violate the resourcehandle’s invariant (§2.4.3.2). For example, the default copy would leave a copy of a Vector referring to the same elements as the original:void bad_copy(Vector v1){Vector v2 = v1;// copy v1’s representation into v2v1[0] = 2;// v2[0] is now also 2!v2[1] = 3;// v1[1] is now also 3!}Assuming that v1 has four elements, the result can be represented graphically like this:v1:v2:4423Fortunately, the fact that Vector has a destructor is a strong hint that the default (memberwise) copysemantics is wrong and the compiler should at least warn against this example (§17.6).
We need todefine better copy semantics.Copying of an object of a class is defined by two members: a copy constructor and a copyassignment:class Vector {private:double∗ elem; // elem points to an array of sz doublesint sz;public:Vector(int s);// constructor: establish invariant, acquire resources˜Vector() { delete[] elem; }// destructor: release resourcesVector(const Vector& a);Vector& operator=(const Vector& a);// copy constructor// copy assignmentdouble& operator[](int i);const double& operator[](int i) const;int size() const;};A suitable definition of a copy constructor for Vector allocates the space for the required number ofelements and then copies the elements into it, so that after a copy each Vector has its own copy ofthe elements:74A Tour of C++: Abstraction MechanismsVector::Vector(const Vector& a):elem{new double[sz]},sz{a.sz}{for (int i=0; i!=sz; ++i)elem[i] = a.elem[i];}Chapter 3// copy constructor// allocate space for elements// copy elementsThe result of the v2=v1 example can now be presented as:v1:v2:4243Of course, we need a copy assignment in addition to the copy constructor:Vector& Vector::operator=(const Vector& a){double∗ p = new double[a.sz];for (int i=0; i!=a.sz; ++i)p[i] = a.elem[i];delete[] elem;// delete old elementselem = p;sz = a.sz;return ∗this;}// copy assignmentThe name this is predefined in a member function and points to the object for which the memberfunction is called.A copy constructor and a copy assignment for a class X are typically declared to take an argument of type const X&.3.3.2 Moving ContainersWe can control copying by defining a copy constructor and a copy assignment, but copying can becostly for large containers.
Consider:Vector operator+(const Vector& a, const Vector& b){if (a.size()!=b.size())throw Vector_size_mismatch{};Vector res(a.size());for (int i=0; i!=a.size(); ++i)res[i]=a[i]+b[i];return res;}Section 3.3.2Moving ContainersReturning from a + involves copying the result out of the local variablewhere the caller can access it. We might use this + like this:res75and into some placevoid f(const Vector& x, const Vector& y, const Vector& z){Vector r;// ...r = x+y+z;// ...}That would be copying a Vector at least twice (one for each use of the + operator). If a Vector islarge, say, 10,000 doubles, that could be embarrassing.
The most embarrassing part is that res inoperator+() is never used again after the copy. We didn’t really want a copy; we just wanted to getthe result out of a function: we wanted to move a Vector rather than to copy it. Fortunately, we canstate that intent:class Vector {// ...Vector(const Vector& a);// copy constructorVector& operator=(const Vector& a);// copy assignmentVector(Vector&& a);Vector& operator=(Vector&& a);// move constructor// move assignment};Given that definition, the compiler will choose the move constructor to implement the transfer ofthe return value out of the function. This means that r=x+y+z will involve no copying of Vectors.Instead, Vectors are just moved.As is typical, Vector’s move constructor is trivial to define:Vector::Vector(Vector&& a):elem{a.elem},// "grab the elements" from asz{a.sz}{a.elem = nullptr;// now a has no elementsa.sz = 0;}The && means ‘‘rvalue reference’’ and is a reference to which we can bind an rvalue (§6.4.1).
Theword ‘‘rvalue’’ is intended to complement ‘‘lvalue,’’ which roughly means ‘‘something that canappear on the left-hand side of an assignment.’’ So an rvalue is – to a first approximation – a valuethat you can’t assign to, such as an integer returned by a function call, and an rvalue reference is areference to something that nobody else can assign to. The res local variable in operator+() for Vectors is an example.A move constructor does not take a const argument: after all, a move constructor is supposed toremove the value from its argument. A move assignment is defined similarly.A move operation is applied when an rvalue reference is used as an initializer or as the righthand side of an assignment.76A Tour of C++: Abstraction MechanismsChapter 3After a move, a moved-from object should be in a state that allows a destructor to be run.
Typically, we should also allow assignment to a moved-from object (§17.5, §17.6.2).Where the programmer knows that a value will not be used again, but the compiler can’t beexpected to be smart enough to figure that out, the programmer can be specific:Vector f(){Vector x(1000);Vector y(1000);Vector z(1000);// ...z = x;y = std::move(x);// ...return z;};// we get a copy// we get a move// we get a moveThe standard-library function move() returns an rvalue reference to its argument.Just before the return we have:z:x:100012nullptr...y:0100012...When z is destroyed, it too has been moved from (by the return) so that, like x, it is empty (it holdsno elements).3.3.3 Resource ManagementBy defining constructors, copy operations, move operations, and a destructor, a programmer canprovide complete control of the lifetime of a contained resource (such as the elements of a container).
Furthermore, a move constructor allows an object to move simply and cheaply from onescope to another. That way, objects that we cannot or would not want to copy out of a scope can besimply and cheaply moved out instead. Consider a standard-library thread representing a concurrent activity (§5.3.1) and a Vector of a million doubles. We can’t copy the former and don’t want tocopy the latter.std::vector<thread> my_threads;Vector init(int n){thread t {heartbeat};my_threads.push_back(move(t));// ... more initialization ...// run hear tbeat concurrently (on its own thread)// move t into my_threadsSection 3.3.3Resource Management77Vector vec(n);for (int i=0; i<vec.size(); ++i) vec[i] = 777;return vec;// move res out of init()}auto v = init(); // star t hear tbeat and initialize vThis makes resource handles, such as Vector and thread, an alternative to using pointers in manycases.
In fact, the standard-library ‘‘smart pointers,’’ such as unique_ptr, are themselves resourcehandles (§5.2.1).I used the standard-library vector to hold the threads because we don’t get to parameterizeVector with an element type until §3.4.1.In very much the same way as new and delete disappear from application code, we can makepointers disappear into resource handles. In both cases, the result is simpler and more maintainablecode, without added overhead. In particular, we can achieve strong resource safety; that is, we caneliminate resource leaks for a general notion of a resource.
Examples are vectors holding memory,threads holding system threads, and fstreams holding file handles.3.3.4 Suppressing OperationsUsing the default copy or move for a class in a hierarchy is typically a disaster: given only a pointerto a base, we simply don’t know what members the derived class has (§3.2.2), so we can’t knowhow to copy them. So, the best thing to do is usually to delete the default copy and move operations, that is, to eliminate the default definitions of those two operations:class Shape {public:Shape(const Shape&) =delete;Shape& operator=(const Shape&) =delete;Shape(Shape&&) =delete;Shape& operator=(Shape&&) =delete;// no copy operations// no move operations˜Shape();// ...};Now an attempt to copy a Shape will be caught by the compiler. If you need to copy an object in aclass hierarchy, write some kind of clone function (§22.2.4).In this particular case, if you forgot to delete a copy or move operation, no harm is done. Amove operation is not implicitly generated for a class where the user has explicitly declared a destructor.
Furthermore, the generation of copy operations is deprecated in this case (§44.2.3). Thiscan be a good reason to explicitly define a destructor even where the compiler would have implicitly provided one (§17.2.3).A base class in a class hierarchy is just one example of an object we wouldn’t want to copy. Aresource handle generally cannot be copied just by copying its members (§5.2, §17.2.2).The =delete mechanism is general, that is, it can be used to suppress any operation (§17.6.4).78A Tour of C++: Abstraction MechanismsChapter 33.4 TemplatesSomeone who wants a vector is unlikely always to want a vector of doubles.
A vector is a generalconcept, independent of the notion of a floating-point number. Consequently, the element type of avector ought to be represented independently. A template is a class or a function that we parameterize with a set of types or values. We use templates to represent concepts that are best understoodas something very general from which we can generate specific types and functions by specifyingarguments, such as the element type double.3.4.1 Parameterized TypesWe can generalize our vector-of-doubles type to a vector-of-anything type by making it aand replacing the specific type double with a parameter. For example:templatetemplate<typename T>class Vector {private:T∗ elem; // elem points to an array of sz elements of type Tint sz;public:Vector(int s);// constructor: establish invariant, acquire resources˜Vector() { delete[] elem; }// destructor: release resources// ...
copy and move operations ...T& operator[](int i);const T& operator[](int i) const;int size() const { return sz; }};The template<typename T> prefix makes T a parameter of the declaration it prefixes. It is C++’s version of the mathematical ‘‘for all T’’ or more precisely ‘‘for all types T.’’The member functions might be defined similarly:template<typename T>Vector<T>::Vector(int s){if (s<0) throw Negative_size{};elem = new T[s];sz = s;}template<typename T>const T& Vector<T>::operator[](int i) const{if (i<0 || size()<=i)throw out_of_range{"Vector::operator[]"};return elem[i];}Section 3.4.1Parameterized Types79Given these definitions, we can define Vectors like this:Vector<char> vc(200);Vector<string> vs(17);Vector<list<int>> vli(45);// vector of 200 characters// vector of 17 strings// vector of 45 lists of integersThe >> in Vector<list<int>> terminates the nested template arguments; it is not a misplaced inputoperator.
It is not (as in C++98) necessary to place a space between the two >s.We can use Vectors like this:void write(const Vector<string>& vs){for (int i = 0; i!=vs.size(); ++i)cout << vs[i] << '\n';}// Vector of some stringsTo support the range-for loop for our Vector, we must define suitable begin() and end() functions:template<typename T>T∗ begin(Vector<T>& x){return &x[0];// pointer to first element}template<typename T>T∗ end(Vector<T>& x){return x.begin()+x.size(); // pointer to one-past-last element}Given those, we can write:void f2(const Vector<string>& vs) // Vector of some strings{for (auto& s : vs)cout << s << '\n';}Similarly, we can define lists, vectors, maps (that is, associative arrays), etc., as templates (§4.4,§23.2, Chapter 31).Templates are a compile-time mechanism, so their use incurs no run-time overhead compared to‘‘handwritten code’’ (§23.2.2).3.4.2 Function TemplatesTemplates have many more uses than simply parameterizing a container with an element type.