Article 2: Transfer Using the right value reference
This is the second article in the series about efficient value types in C ++. In the previous article, we discussed how to eliminate the possibility of multiple replication operations. Replication Omitting is transparent and automatically occurs in code that looks very common, with almost no disadvantages. There are already enough good news. Let's take a look at the bad news:
- Copying omitted is not required by the standard, so you cannot write the portable code that can ensure it will happen.
- Sometimes this cannot be done. For example:
return q ? var1 : var2;
The caller can use the memory provided by the caller for either var1 or var2. If it chooses to save var1 in the memory, and Q is false, var2 will still be copied (and vice versa ).
- Replication Omitting is likely to exceed the compiler's stack space allocation skills.
Inefficient Transfer
There are many opportunities for optimization when an operation is to rearrange data. Take a simple generic insertion sorting algorithm as an example:
template <class Iter> void insertion_sort(Iter first, Iter last) { if (first == last) return; Iter i = first; while (++i != last) // Invariant: elements preceding i are sorted { Iter next = i, prev = i; if (*--prev > *i) { typename std::iterator_traits<Iter>::value_type x(*next); do *next = *prev; while(--next != first && *--prev > x); *next = x; } } }
Row 3: Unchanged outer loop
Row 12th: Copies the first unordered element to a temporary position.
Row 13th: copy the last sorted element backward.
Row 13th: Continue to copy back until a proper location is found.
Row 15th: copy the elements in the temporary position to the correct position.
Imagine what would happen if the elements in the sequencing sequence are STD: vector <STD: String>: in rows 12th, 13, and 15, we need to potentially copy a string vector, which leads to a large amount of memory allocation and data replication.
Sorting operations are essentially a type of data conservation operation, so the overhead of data replication should be avoided: in principle, what we really need to do is to move the object in the sequence.
Note that the value of the source object will not be used in all cases. Sounds familiar, right? Yes. This is also true when the source object is the right value. However, this time the source object is left: these objects all have addresses.
Can the reference count be used?
A common method to solve this type of inefficiency is to allocate elements on the stack and hold the reference count smart pointer pointing to these elements in the sequence (container), rather than directly saving these elements. The reference counting smart pointer is similar to a common pointer, but it also tracks how many reference counting smart pointers point to the same object and destroys the object when the last smart pointer is deleted. To copy a reference counting pointer, you only need to increase the reference counting. This is very fast. If you assign a value to the reference counting pointer, one reference counting is incremented and the other is decreased. This is also very fast.
So, can it be faster? Of course, it is not counted at all! In addition, the reference count has other weaknesses we want to avoid:
- Its overhead is very large in multi-threaded environments, because the count itself needs to be shared across threads, which requires synchronization.
- In generic code, this method is invalid because the element type may be a lightweight type like Int. In this case, the increase or decrease of the reference count is the real performance overhead. You either have to endure this overhead, or you have to introduce a complex framework to determine which types are lightweight and should be saved directly, and access these values in a unified style.
- Reference semantics makes the code hard to read. For example:
typedef std::vector<std::shared_ptr<std::string> > svec;…svec s2 = s1;std::for_each( s2.begin(), s2.end(), to_uppercase() );
Changing S2 to uppercase will also change to the value of S1. This is a much larger topic than we have discussed here. In short, when data sharing is hidden, it seems that the effect of local modifications is not necessarily partial.
Introduce the right value reference of C ++ 0x
To solve these problems, C ++ 0x introduces a new reference with the right value reference. T's right value reference writing T & (read as "Tee ref-Ref"). Now we call the original T & reference as "Left value reference ". As far as the scope is discussed, the main difference between the left value reference and the right value reference is that a non-const right value reference can be bound to the right value. Many C ++ programmers have encountered such errors:
invalid initialization of non-const reference of type 'X&' from a temporary of type 'X'
Such prompts are usually caused by the following code:
X f(); // call to f yields an rvalueint g(X&);int x = g( f() ); // error
According to the standard, non-const (left value) references should be bound to a left value, rather than a temporary object (that is, a right value ). This makes sense, because any modifications to the temporary object to which the reference is directed will be lost. In contrast, the non-const right value reference should be bound to a temporary object, rather than a left value:
X f();X a;int g(X&&); int b = g( f() ); // OKint c = g( a ); // ERROR: can't bind rvalue reference to an lvalue
Steal Resources
Assume that our function g () needs to save a copy of its parameters for future use:
static X cache; int g(X&& a){ cache = a; // keep it for later} int b = g( X() ); // call g with a temporary
Depending on Type X, this replication may be costly and may cause memory allocation and deep replication of many sub-objects.
Since the G () parameter is a reference to the right value, we know that it can only be automatically bound to an anonymous temporary object, rather than other objects. Therefore,
- Shortly after we copy this temporary object to the cache, the copied source object will be destroyed.
- Any modification to this temporary object is invisible to other parts of the program.
This gives us a chance to execute some new optimizations and avoid unnecessary work by modifying the value of the temporary object. One of the most common optimizations is resource theft.
Resource stealing refers to removing resources (such as memory and large sub-objects) from one object and transferring resources to another object. For example, the string class may have a character buffer allocated on the heap. To copy a string, you need to allocate a new buffer and copy all characters to the new buffer. This looks very slow. To steal a string, you only need to let another object take the string buffer and notify the source object that it no longer has a valid buffer-this operation is much faster. With the right-value reference, we can optimize our code by copying a temporary object to steal a temporary object. At the same time, because only temporary objects are changed, this optimization is logically not rewritten.
Description: Stealing (or modifying) from the right value reference can be logically considered as a non-rewriting operation.
Reload the right value
From the above description, we can get a new semantic-preserving programming change: we can use another version that accepts the right value reference at the same position to accept one (const) overload any function that references parameters:
void g(X const& a) { … } // doesn't mutate argumentvoid g(X&& a) { modify(a); } // new overload; logically non-mutating
The second overload version of G can modify its parameters, but it does not affect other parts of the program, so it has the same semantics as the first overload version.
Binding and overloading
The following table summarizes the complete rules of C ++ 0x for reference binding and overloading:
Expression → Reference Type |
T right value |
Right Value of const t |
T left |
Const T left |
Priority |
T && |
X |
|
|
|
4 |
Const T && |
X |
X |
|
|
3 |
T & |
|
|
X |
|
2 |
Const T & |
X |
X |
X |
X |
1 |
The "Priority" column describes the actions of these references in heavy-load resolutions. For example, the following overload is provided:
void f(int&&); // #1void f(const int&&); // #2void f(const int&); // #3
If you pass the right value of a const int type to F, #2 is called because #1 cannot be bound, and #3 has a lower priority.
Declare a convertible type
With the above method, we can use two new operations, transfer construction and transfer assignment to make the right value of any type convertible by implicit transfer, both operations accept the right value reference parameter. For example, a convertible STD: vector may write in C ++ 0x as follows:
template <class T, class A>struct vector{ vector(vector const& lvalue); // copy constructor vector& operator=(vector const& lvalue); // copy assignment operator vector(vector&& rvalue); // move constructor vector& operator=(vector&& rvalue); // move assignment operator …};
The function of the transfer constructor and the transfer value assignment operator is to "steal" resources from its parameters, and then place the parameters in a configurable or allocable state.
In the STD: vector example, this may mean that the parameter is set back to the status of the empty container. A typical STD: vector Implementation contains three pointers: one pointing to the starting point of the allocated space, the other pointing to the last element, and the other pointing to the end of the allocated space. Therefore, when the container is empty, all three pointers are null, And the transfer constructor will look like this:
vector(vector&& rhs) : start(rhs.start) // adopt rhs's storage , elements_end(rhs.elements_end) , storage_end(rhs.storage_end){ // mark rhs as empty. rhs.start = rhs.elements_end = rhs.storage_end = 0;}
The transfer assignment operator may be like this:
vector& operator=(vector&& rhs){ std::swap(*this, rhs); return *this;}
Because the right value parameter will be destroyed immediately, the exchange operation not only obtains its resources, but also "arranges" the resources we originally had to be destroyed.
Note:: Don't be so happy. This transfer assignment operator is not very correct.
Right Value reference and copy omitted
STD: the overhead of the Vector's transfer constructor is very low (only three reads and six writes to the memory), but it is not free. Fortunately, the criteria indicate that the priority of the omitted copy (which is really cost-effective) is higher than that of the transfer operation. When you pass a right value as a value or return a value from a function, the compiler should first eliminate replication. If the replication cannot be eliminated and the corresponding type has a transfer constructor, the compiler is required to use the transfer constructor. Finally, if no transfer constructor exists, the compiler can only use the copy constructor.
Example:
A compute(…){ A v; … return v;}
- If a has an accessible replication constructor or a transfer constructor, the compiler can choose to remove replication.
- Otherwise, if A has a transfer constructor, V is transferred.
- Otherwise, if A has a copy constructor, V is copied.
- Otherwise, the compiler reports an error.
Therefore, the guidelines in the previous article are still valid:
Guidelines: Do not copy your function parameters. Instead, it should be passed in the way of passing values, so that the compiler can perform replication.
With this guidance, you may ask: "In addition to the transfer constructor and the transfer value assignment operator, where can I use the right value overload? Once all my types are convertible, what else can I do ?" See the following example.
Transfer from left
All of these transfer optimizations share a common point: optimization can be performed only when the source object is no longer used. But sometimes we need to remind the compiler. For example:
void g(X); void f(){ X b; g(b); … g(b);}
In row 8th, we call G with a left value, so that we cannot steal resources-even if we know that B will no longer be used. To tell the compiler that it can be transferred from B, we can use STD: Move to pass it:
void g(X); void f(){ X b; g(b); // still need the value of b … g( std::move(b) ); // all done with b now; grant permission to move}
Note: STD: Move itself does not perform any transfer. It only changes the parameter to a right reference, so that the transfer optimization can be used in an environment that complies with the transfer optimization. When you see STD: Move, you can think like this: grant the transfer permission. You can also regard STD: Move (a) as the description of static_cast <X &> (.
Efficient Transfer
Now we have a way to transfer the left value. We can optimize the insertion_sort algorithm in the previous sections:
template void insertion_sort(Iter first, Iter last) { if (first == last) return; Iter i = first; while (++i != last) // Invariant: [first, i) is sorted { Iter next = i, prev = i; if (*--prev > *i) { typename std::iterator_traits::value_type x( std::move(*next) ); do *next = std::move(*prev); while(--next != first && *--prev > x); *next = std::move(x); } }}
Row 12th: Move the first unordered element to a temporary position.
Row 13th: Move the last sorted element to the back
Row 3: Move back
Row 15th: Move the elements in the temporary position to the correct position.
In addition to the format differences, the difference between this version and the previous version is that the call to STD: move is added. It is worth noting that we only need the implementation of this insertion_sort, regardless of whether the element type has a transfer constructor. This is a typical transfer-able code: the design of the right value reference allows you to "transfer when possible, copy when necessary ".
Subsequent content
I am here for the time being, but this series of articles will continue (soon, I promise you-the materials have been written !), The content will cover the right-side resurrection, exceptional security, and perfect forward conversion. Oh, by the way, we will also tell you how to correctly write the vector's transfer value assignment operator. See you later!