Muestra las diferencias entre dos versiones de la página.
| Ambos lados, revisión anterior Revisión previa Próxima revisión | Revisión previa | ||
|
wiki2:cpp:idiomatic [2015/11/03 11:29] alfred [POD types] |
wiki2:cpp:idiomatic [2020/05/09 09:25] (actual) |
||
|---|---|---|---|
| Línea 3: | Línea 3: | ||
| ===== Idioms ===== | ===== Idioms ===== | ||
| - | ===== POD types ===== | + | ==== POD types ==== |
| POD type (Plain Old Data, also Passive data structure) is, a class (whether defined with the keyword struct or the keyword class) without constructors, destructors and virtual members functions. | POD type (Plain Old Data, also Passive data structure) is, a class (whether defined with the keyword struct or the keyword class) without constructors, destructors and virtual members functions. | ||
| Línea 41: | Línea 41: | ||
| </code> | </code> | ||
| - | ====== The rule of three and the copy-and-swap idiom ====== | + | ==== The rule of three and the copy-and-swap idiom ==== |
| + | There are two ways to copy an object in C++: the copy constructor or with the assignment. | ||
| + | <code cpp> | ||
| + | class person | ||
| + | { | ||
| + | std::string name; | ||
| + | int age; | ||
| + | |||
| + | public: | ||
| + | |||
| + | person(const std::string& name, int age) : name(name), age(age) | ||
| + | { | ||
| + | } | ||
| + | }; | ||
| + | |||
| + | int main() | ||
| + | { | ||
| + | person a("Bjarne Stroustrup", 60); | ||
| + | person b(a); // What happens here? | ||
| + | b = a; // And here? | ||
| + | } | ||
| + | </code> | ||
| + | The rule of three says that the copy constructor and copy assignment operator, and destructor are special member functions. The implementation will implicitly declare these member functions for some class types when the program does not explicitly declare them. The implementation will implicitly define them if they are used. | ||
| + | |||
| + | The implicit definition wolud be: | ||
| + | <code cpp> | ||
| + | person(const person& that) : name(that.name), age(that.age) | ||
| + | { | ||
| + | } | ||
| + | |||
| + | // 2. copy assignment operator | ||
| + | person& operator=(const person& that) | ||
| + | { | ||
| + | name = that.name; | ||
| + | age = that.age; | ||
| + | return *this; | ||
| + | } | ||
| + | |||
| + | // 3. destructor | ||
| + | ~person() | ||
| + | { | ||
| + | } | ||
| + | </code> | ||
| + | |||
| + | However when they manage resources (pointers and so) it's not so straightforward: | ||
| + | <code cpp> | ||
| + | class person | ||
| + | { | ||
| + | char* name; | ||
| + | int age; | ||
| + | |||
| + | public: | ||
| + | |||
| + | // the constructor acquires a resource: | ||
| + | // in this case, dynamic memory obtained via new[] | ||
| + | person(const char* the_name, int the_age) | ||
| + | { | ||
| + | name = new char[strlen(the_name) + 1]; | ||
| + | strcpy(name, the_name); | ||
| + | age = the_age; | ||
| + | } | ||
| + | |||
| + | // the destructor must release this resource via delete[] | ||
| + | ~person() | ||
| + | { | ||
| + | delete[] name; | ||
| + | } | ||
| + | }; | ||
| + | </code> | ||
| + | However this can bring some unpleasant effects: | ||
| + | - Changes via a can be observed via b. | ||
| + | - Once b is destroyed, a.name is a dangling pointer. | ||
| + | - If a is destroyed, deleting the dangling pointer yields undefined behavior. | ||
| + | - Since the assignment does not take into account what name pointed to before the assignment, sooner or later you will get memory leaks all over the place. | ||
| + | |||
| + | There is a naive implementation that solves these problems: | ||
| + | <code cpp> | ||
| + | person(const person& that) | ||
| + | { | ||
| + | name = new char[strlen(that.name) + 1]; | ||
| + | strcpy(name, that.name); | ||
| + | age = that.age; | ||
| + | } | ||
| + | |||
| + | // 2. copy assignment operator | ||
| + | person& operator=(const person& that) | ||
| + | { | ||
| + | if (this != &that) | ||
| + | { | ||
| + | delete[] name; | ||
| + | // This is a dangerous point in the flow of execution! | ||
| + | // We have temporarily invalidated the class invariants, | ||
| + | // and the next statement might throw an exception, | ||
| + | // leaving the object in an invalid state :( | ||
| + | name = new char[strlen(that.name) + 1]; | ||
| + | strcpy(name, that.name); | ||
| + | age = that.age; | ||
| + | } | ||
| + | return *this; | ||
| + | } | ||
| + | </code> | ||
| + | |||
| + | Or you also can make those resources non copiable: | ||
| + | <code cpp> | ||
| + | private: | ||
| + | person(const person& that); | ||
| + | person& operator=(const person& that); | ||
| + | </code> | ||
| + | |||
| + | === copy-and-swap idiom === | ||
| + | |||
| + | The copy-and-swap idiom is the solution, and elegantly assists the assignment operator in achieving two things: avoiding code duplication, and providing a strong exception guarantee. | ||
| + | |||
| + | Conceptually, it works by using the copy-constructor's functionality to create a local copy of the data, then takes the copied data with a swap function, swapping the old data with the new data. The temporary copy then destructs, taking the old data with it. We are left with a copy of the new data. | ||
| + | |||
| + | In order to use the copy-and-swap idiom, we need three things: a working copy-constructor, a working destructor (both are the basis of any wrapper, so should be complete anyway), and a swap function. | ||
| + | |||
| + | A swap function is a non-throwing function that swaps two objects of a class, member for member. We might be tempted to use std::swap instead of providing our own, but this would be impossible; std::swap uses the copy-constructor and copy-assignment operator within its implementation, and we'd ultimately be trying to define the assignment operator in terms of itself! | ||
| + | |||
| + | Let's consider a concrete case. We want to manage, in an otherwise useless class, a dynamic array. We start with a working constructor, copy-constructor, and destructor: | ||
| + | <code cpp> | ||
| + | #include <algorithm> // std::copy | ||
| + | #include <cstddef> // std::size_t | ||
| + | |||
| + | class dumb_array | ||
| + | { | ||
| + | public: | ||
| + | // (default) constructor | ||
| + | dumb_array(std::size_t size = 0) | ||
| + | : mSize(size), | ||
| + | mArray(mSize ? new int[mSize]() : 0) | ||
| + | { | ||
| + | } | ||
| + | |||
| + | // copy-constructor | ||
| + | dumb_array(const dumb_array& other) | ||
| + | : mSize(other.mSize), | ||
| + | mArray(mSize ? new int[mSize] : 0), | ||
| + | { | ||
| + | // note that this is non-throwing, because of the data | ||
| + | // types being used; more attention to detail with regards | ||
| + | // to exceptions must be given in a more general case, however | ||
| + | std::copy(other.mArray, other.mArray + mSize, mArray); | ||
| + | } | ||
| + | |||
| + | // destructor | ||
| + | ~dumb_array() | ||
| + | { | ||
| + | delete [] mArray; | ||
| + | } | ||
| + | |||
| + | private: | ||
| + | std::size_t mSize; | ||
| + | int* mArray; | ||
| + | }; | ||
| + | </code> | ||
| + | This class almost manages the array successfully, but it needs operator= to work correctly. | ||
| + | |||
| + | Here's how a naive implementation might look: | ||
| + | <code cpp> | ||
| + | // the hard part | ||
| + | dumb_array& operator=(const dumb_array& other) | ||
| + | { | ||
| + | if (this != &other) // (1) | ||
| + | { | ||
| + | // get rid of the old data... | ||
| + | delete [] mArray; // (2) | ||
| + | mArray = 0; // (2) *(see footnote for rationale) | ||
| + | |||
| + | // ...and put in the new | ||
| + | mSize = other.mSize; // (3) | ||
| + | mArray = mSize ? new int[mSize] : 0; // (3) | ||
| + | std::copy(other.mArray, other.mArray + mSize, mArray); // (3) | ||
| + | } | ||
| + | |||
| + | return *this; | ||
| + | } | ||
| + | </code> | ||
| + | And we say we're finished; this now manages an array, without leaks. However, it suffers from three problems, marked sequentially in the code as (n). | ||
| + | |||
| + | (1) The first is the self-assignment test. This check serves two purposes: it's an easy way to prevent us from running needless code on self-assignment, and it protects us from subtle bugs (such as deleting the array only to try and copy it). But in all other cases it merely serves to slow the program down, and act as noise in the code; self-assignment rarely occurs, so most of the time this check is a waste. It would be better if the operator could work properly without it. | ||
| + | |||
| + | (2)The second is that it only provides a basic exception guarantee. If new int[mSize] fails, *this will have been modified. (Namely, the size is wrong and the data is gone!) For a strong exception guarantee, it would need to be something akin to: | ||
| + | <code cpp> | ||
| + | dumb_array& operator=(const dumb_array& other) | ||
| + | { | ||
| + | if (this != &other) // (1) | ||
| + | { | ||
| + | // get the new data ready before we replace the old | ||
| + | std::size_t newSize = other.mSize; | ||
| + | int* newArray = newSize ? new int[newSize]() : 0; // (3) | ||
| + | std::copy(other.mArray, other.mArray + newSize, newArray); // (3) | ||
| + | |||
| + | // replace the old data (all are non-throwing) | ||
| + | delete [] mArray; | ||
| + | mSize = newSize; | ||
| + | mArray = newArray; | ||
| + | } | ||
| + | |||
| + | return *this; | ||
| + | } | ||
| + | </code> | ||
| + | (3) The code has expanded! Which leads us to the third problem: code duplication. Our assignment operator effectively duplicates all the code we've already written elsewhere, and that's a terrible thing. | ||
| + | |||
| + | We want this: | ||
| + | <code cpp> | ||
| + | class dumb_array | ||
| + | { | ||
| + | public: | ||
| + | // ... | ||
| + | |||
| + | friend void swap(dumb_array& first, dumb_array& second) // nothrow | ||
| + | { | ||
| + | // enable ADL (not necessary in our case, but good practice) | ||
| + | using std::swap; | ||
| + | |||
| + | // by swapping the members of two classes, | ||
| + | // the two classes are effectively swapped | ||
| + | swap(first.mSize, second.mSize); | ||
| + | swap(first.mArray, second.mArray); | ||
| + | } | ||
| + | |||
| + | // ... | ||
| + | }; | ||
| + | </code> | ||
| + | With... | ||
| + | <code cpp> | ||
| + | dumb_array& operator=(dumb_array other) // (1) | ||
| + | { | ||
| + | swap(*this, other); // (2) | ||
| + | |||
| + | return *this; | ||
| + | } | ||
| + | </code> | ||
| + | e first notice an important choice: the parameter argument is taken by-value. While one could just as easily do the following (and indeed, many naive implementations of the idiom do): | ||
| + | <code cpp> | ||
| + | dumb_array& operator=(const dumb_array& other) | ||
| + | { | ||
| + | dumb_array temp(other); | ||
| + | swap(*this, temp); | ||
| + | |||
| + | return *this; | ||
| + | } | ||
| + | </code> | ||
| + | |||
| + | The next version of C++, C++11, makes one very important change to how we manage resources: the Rule of Three is now The Rule of Four (and a half). Why? Because not only do we need to be able to copy-construct our resource, we need to move-construct it as well. | ||
| + | |||
| + | Luckily for us, this is easy: | ||
| + | <code cpp> | ||
| + | class dumb_array | ||
| + | { | ||
| + | public: | ||
| + | // ... | ||
| + | |||
| + | // move constructor | ||
| + | dumb_array(dumb_array&& other) | ||
| + | : dumb_array() // initialize via default constructor, C++11 only | ||
| + | { | ||
| + | swap(*this, other); | ||
| + | } | ||
| + | |||
| + | // ... | ||
| + | }; | ||
| + | </code> | ||
| + | * https://stackoverflow.com/questions/3279543/what-is-the-copy-and-swap-idiom | ||
| + | * https://stackoverflow.com/questions/4172722/what-is-the-rule-of-three | ||
| ==== Value categories ==== | ==== Value categories ==== | ||