Instead KDPV - short drama to attract attention, based on real events. You can safely skip it and go to the article that will help you understand the rvalue links, displacement constructors, universal links, perfect forwarding, etc.
First act
Compiler Local object x of type T, living on the stack, you are sentenced to seize from you all the property due to the fact that you will not use it for the rest of your life.
Object x . What? I am not a temporary facility, I have a permanent registration, you do not have the right!
Compiler Nobody evicts you. But according to the eleventh edition of the standard code, all your belongings will be transferred to another object, which needs them more.
Object x . And how do you do it? All my data is securely encapsulated, I will not allow anyone to treat them unceremoniously. If you really need them, then let the copy designer come with his flash drive, I'll copy to him.
Constructor . It is long and ineffective.
Object x . Maybe you are going to break the reinterpret_cast window for me, and rummage in the dark?
Compiler No, no, your case is too ordinary to use the services of collectors. We just call the std :: move function, it puts static_cast <T &&> on you, and everyone will think that you are a temporary object, that is, an rvalue expression.
Object x . So what, static_cast does not affect me. As soon as I get to the first point, I will take it off.
Compiler I have no doubt, but sooner than you do this, you will encounter a displacement designer already waiting outside the door. It seems you have not had time to meet him ...
Second act
T (T &&) . Oh, hello, temporary object!
Object x . No-no-no, I'm lvalue, just static_cast on me.
T (T &&) . Everyone says like that. Well, what do you have? Come here...
Act Three (epilogue)
T (const T &) . I'm not happy with your violent methods.
T (T &&) . You exaggerate, think for yourself why a dying object all this stuff? If it were not for me, you would have made a copy for a long time, and immediately after you the destructor would have destroyed the original. Stupid.
T (const T &) . So maybe he did not lie, and he is not temporary?
T (T &&) . I do not care. So someone decided that it was necessary. Everything is legal, I just do my job.
T (const T &) . But earlier they coped well without you. Somehow they organized the transfer and return, or allocated to the object living space in dynamic memory. And the compiler helped with optimization.
T (T &&) . Oh, remember all this bureaucracy. The object was settled, then the registration record was lost, and that was all; he lived there until the end, and did not drive it away. Enough to regret these objects, otherwise you will turn into my brother T (const T &&) - that one is even more compassionate. I said to him: “Everything, the object is no longer a tenant, take away his belongings,” and he hesitates, saying uncomfortable, let me just copy it.
T (const T &) . I also have a brother T (T &), a real gangster. Disguised as me, under the copy designer ... Then come up with laziness .
New concepts do not always fit exactly in the head. It happened to me with rvalue links. It seems that everything is very simple, the preconditions for their appearance are clear, but when you try to read a code saturated with various && wrapped in a bunch of templates, you realize that you don’t understand anything.
My mistake in studying this topic was that I presented the rvalue-links as a fundamentally new entity. Perhaps this will seem strange to you, because in all the manuals they clearly say that this is just a link to rvalue. I understood that. But after all, a bunch of new concepts have appeared with them, such as universal links and perfect forwarding. Also, the call to the function that returns && has become some kind of mystical xvalue expression. In short, it would be too easy to consider them as normal links.
So, most importantly - do not complicate! If you saw T&& ref = foo()
, and do not know how to treat ref now, then treat it as an old-kind constant reference to rvalue: const T& ref = foo()
, only without const.
And why it was impossible to simply allow to take a link to rvalue? Otherwise, we would immediately lose information about whether the expression was lvalue or rvalue. Now, the rvalue "prefers" to be passed to the function with the argument T &&, and the lvalue - with T &. This gives us the opportunity to deal with objects in different ways, which is especially important for the implementation of copy and move constructors.
Another of my mistakes is checking samples in Visual Studio. Examples in articles, for example std::string &str = std::string("42")
, which should not be compiled, have been compiled. This was due to a non-standard language extension from Visual Studio. I will talk about this again, because understanding this behavior is very important if VS is your development environment.
The best way to read this article is not to believe me and check everything yourself. I advise in the debug build. If you are using GCC, it would be nice to -fno-elide-constructors
switch to disable the Copy Elision technique, which suppresses the challenge of copy and move constructors where you can. And if VS, then turn on the 4th level of warnings for catching use of non-standard extensions.
When studying rvalue links and displacement constructors, you can often find similar examples:
Matrix m = m1 * m2; std::string s = s1 + s2; std::vector<BigInt> primes = getAllMersennePrimes();
The temporary object is copied and immediately destroyed. This, of course, is clearly an redundant operation, and the work of the displacement constructor is quite obvious here. However, by adding a move constructor to your class, you may not notice the acceleration. After all, the compiler uses various optimization techniques, in particular Return Value Optimization, which we will talk about a bit at the end of the article. I offer the following example. Imagine that we have filled a large local vector:
std::vector<int> items(1000000); fill(items);
In memory, it looks like this (we will not over-complicate the scheme with a third pointer to the end of the reserve memory):
And we want to pass it to some object, via setter:
storage->setItems(items); // items
As it was before: the vector was transmitted by a constant reference (which allowed using both lvalue and rvalue), and then the copy constructor was called, which created the same large vector. And the original was deleted only after going out of scope. Although I would just like to give the new vector a pointer to the data:
Now it is easy:
std::vector<int> items(1000000); fill(items); storage->setItems(std::move(items)); // items
And in the setItems method:
Storage::setItems(std::vector<int> items) { items_ = std::move(items); }
Note that the vector is passed by value, which provides a copy for the lvalue and a move for the rvalue (in particular, temporary objects). Thus, if you still need a local vector, just call setItems without std :: move. A small overhead is that the argument is moved again using the move operator.
All we need to do to deal with all the concepts is to focus on different ways of initialization. This may be a variable initialization:
T x = expr; // T x(expr);
Passing the argument:
void foo(T x); foo(expr);
Return value:
T bar() { return expr; }
All these cases will be called initialization, since they are semantically identical. Then consider the type T
It may be one of the following:
I will specify that the type with the pointer also belongs to one of them. For example, A*
is a linkless type, A*&
- lvalue reference, etc. Next, pay attention to the expression expr
. The expression has type and category values ​​(rvalue or lvalue). The type of expression for us will not be as important as one might think. In this topic, the main role is played by the category of the value of the expression: rvalue or lvalue, which for brevity will be called the category of the expression.
Thus, we have 3 options on the left, 2 on the right. Total 6. Before considering them in more detail, let us learn to define a category.
Before c ++ 11, only these 2 categories existed. With the advent of move semantics, the category of rvalue was further split into 2 categories, which we will discuss in the next subsection. So, the category of each expression is either lvalue or rvalue. The standard, of course, describes what applies to each of them, but it is difficult to read.
Scott Myers suggests the following rules:
I don’t really like them, because they are not strict, and there are many subtleties that sometimes change. And, most importantly, when studying a topic, how can I understand whether it is possible to take an address? Well, well, we know that a temporary object cannot. And str
?
std::string &&str = std::string("Hello"); std::string *psrt = &str;
It turns out that yes, it is possible, since str
is an lvalue, although its type is an rvalue reference. Remember this is important.
If necessary, I refer to the cppreference: value category , where the list of expressions is given. Some examples from there:
std::cin
.std::cout << 1
, ++it
."Hello, world!"
.42
, true
, nullptr
.&a
.std::move(x)
.am
, where a is rvalue.As mentioned, the rvalue has broken down into two categories: xvalue (eXpiring lvalue) and prvalue (pure rvalue). And lvalue along with xvalue became glvalue (generalized lvalue). Now, for the greatest accuracy, expressions should be attributed to one of 3 categories. On the diagram, it looks like this:
The xvalue includes the following expressions:
std::move(x)
.am
, where a is rvalue.What were additional categories needed for? The xvalue expression, although it is an rvalue, but has some lvalue properties, for example, it can be polymorphic. Further, we will not need these additional categories, but they are useful to increase their credibility during heated discussions.
According to our estimates came out 6 options. In fact, you need to consider only 3. Because, first, you cannot initialize an rvalue-link with an lvalue-expression. And secondly, the initialization of the object (non-reference type) by some expression, is performed using the copy or pass through constructor through the transfer of the expression (into the constructor) by reference, which reduces these cases to one of three options. As an initial guideline such a scheme:
Normal link.
This only works in two cases.
const T& x = rvalue;
Before c ++ 11, this was the only way to pass a temporary object somewhere. For example, in the copy constructor:
T::T(const T&); T x = T(); // T
or somewhere else:
void readFile(const std::string &); std::string filename = "data"; readFile(filename + ".txt"); // std::string readFile
As you know, a constant link extends the life of a temporary object. Yes, but only while this link is still in scope. Since the temporary object is located on the stack, when leaving the scope or from the function, the compiler is no longer obliged to be responsible for the fate of the object.
Do not be surprised by the example that works perfectly in Visual Studio:
std::vector<int> &v = std::vector<int>({ 1, 2, 3 }); v.push_back(4);
The answer will appear only at the 4th warning level:
warning C4239: nonstandard extension used note: A non-const reference may only be bound to an lvalue
This non-standard extension for C ++ is disabled by the /Za (Disable Language Extensions)
key, but some heders, such as Windows.h
, will not compile because the extension also includes other features.
It's simple: you can't write like that. Be careful, on Habré there is a translation of the article “A Brief Introduction to Rvalue References” (article already in 2008), which is the first in search engines for the request “rvalue”. The example from there is erroneous:
A a; A&& a_ref2 = a; // rvalue
Also, there is an incorrect implementation of std::move
. However, in the comments pointed out the error.
Went to the most interesting. Let's start with the simplest examples:
std::string foo(); std::string&& str = foo(); int&& i = 5;
These links behave like normal. You can submit a link const T &, which can be changed, or recall the extension Visual Studio. The question may arise, why it was impossible to simply use a regular link for all categories of expressions? In VS, this worked perfectly (for classes). The && links make it possible to overload functions and especially constructors not only by the type of expression, but also by category (lvalue or rvalue). Example with 2 constructors:
string(const string&); string(string&&); string s1 = "Hello "; string s2 = "world!"; string s3 = s1 + s2;
The expression s1 + s2
is rvalue, and both constructors are suitable for it (see the diagram at the beginning of the section). Priority will be given to the type string &&. Those who are familiar with motion designers know why this is important. Before we talk about this in more detail, we will deal with priorities.
In most situations, it is enough to know that T&&
"more priority" const T&
. But it is desirable to deal with const T&&
and T&
. Here is an extended diagram:
The rules are simple (in descending order of priority):
const T&
will be glad to all expressions.When you write:
T x = expr; // T x(expr);
constructor is called. Which one A class may have several:
T(const T&); //copy- T(T&); // copy- T(const T&&); // ? T(T&&); //move-
When the constructor is called, the expression is passed by reference according to the scheme above, depending on the category of the expression and the type of reference to the arguments. Now about what each constructor does.
The usual copy constructor. It has the lowest priority, but it can take any expression.
I call him an insidious copy constructor, because he can quite easily modify the source if it was not constant at first. Has priority over normal copy constructor. I doubt that it is used in practice. If there are good examples of its use, write.
This can accept rvalue-expressions, i.e., objects whose life expectancy has come to an end. The dying object says: I am dying, take mine grenades pointers to the data, I no longer need them. And the designer answers: no, I can't, they will remain with you, I can only make a copy. I also do not know a practical example of use.
This constructor is called only for non-constant rvalue. Such an expression is a temporary object or an object static_cast<T&&>
to an rvalue reference using static_cast<T&&>
. Such a transformation does not affect the object in any way, however, such a wrapper expression can be passed via an rvalue reference to the displacement constructor (or to some function). The constructor takes all the pointers to the data and other members of the class and passes them to the new object. Therefore, for the effective work of the move constructor, the class members should be smaller. In the limiting case, you can only store one pointer to the implementation. The Pimpl idiom (pointer to implementation) works well here.
For example, consider the class of the string specified using the pointer to the data on the heap and the length of the string (this is only a conceptual example):
class String { char* data_; size_t length_; public: explicit String(size_t length) { data_ = static_cast<char*>(::operator new(length)); length_ = length; } ~String() { ::free(data_); } };
So the displacement constructor might look like:
String(String&& other) { data_ = other.data_; // length_ = other.length_; // other.data_ = nullptr; // ( ) other.length_ = 0; // }
What would have happened if we hadn’t reset its data (3-4 constructor lines)? Sooner or later, for two twin objects, a destructor would be called who would try to delete the same data two times. But what if only the pointer to the data is zeroed, but not the length? After all, the destructor will work twice normally - this one does not use length. Then the user of the object that suddenly uses the getLength
function will receive incorrect information. Therefore, it is impossible to barbarously handle an object that is no longer needed. In any case, you must leave it empty, but in the correct state. In addition, it can cause movement several times throughout its life.
We already talked about static_cast<T&&>
when discussing the displacement constructor. As you remember, the expression "wrapped" in static_cast<T&&>
, signals that the object can be moved. Then, if a new object is initialized, the move constructor will be called, not the copy. Now we can implement the task from the beginning of the article:
std::vector<int> items(1000000); fill(items); static_cast< std::vector<int>&& > (items); // , storage->setItems( static_cast< std::vector<int>&& > (items) ); // items
You probably know that there is a more convenient way - to call the function std::move
, which is a wrapper over static_cast. To understand how it works, we will write it ourselves and step on the rake, but it will be a useful lesson. Before reading further, you can write it yourself. Of course, this function should be template to accept different types. But for now, for simplicity, we will do for one particular class A. Let’s speculate. If we want to pass an lvalue, then this can only be done using an argument of type A &. Next, we convert it to A &&, and the return type will also be A &&. The call to the function that returns && is an rvalue expression (or rather xvalue), as we wanted.
A&& my_move(A& a) { return static_cast<A&&>(a); }
But the standard std :: move function also accepts rvalue, so it is universal, which is not the case with ours. One solution is simple - add another function with the argument A &&:
A&& my_move(A&& a) { return static_cast<A&&>(a); }
Works for both lvalue and rvalue. But wait, the real std :: move is only one and has the template type of the argument T &&. How so? We will understand further.
So, we found out that the template std :: move accepts an lvalue for the argument T &&. So T && is not an rvalue link. T && somehow turns into T &. But what type of T is instantiated? To understand what's what, let's try to call two overloaded functions for two categories of expressions (rvalue and lvalue):
template<class T> void foo(T&); template<class T> void foo(T&&); A a; foo(a); //lvalue; foo(A()); //rvalue;
In the functions themselves, we will check what type is instantiated by T. You can check the type of the link as follows:
bool is_lvalue = std::is_lvalue_reference<T>::value; bool is_rvalue = std::is_rvalue_reference<T>::value;
It turns out that 3 options are suitable:
foo(lvalue)
entry, when foo(T&)
called, is equivalent to foo<T>(lvalue)
.foo(rvalue)
, when calling foo(T&&)
, is equivalent to foo<T>(rvalue)
.foo(lvalue)
, when calling foo(T&&)
, is equivalent to foo<T&>(lvalue)
.In the following diagram, the signatures on the arrows indicate what type T was instantiated for in this or that case.
The first two variants are predictable, we relied on them. The third one is using the reference collapse rule - link compression, which determines the behavior when a link to a link appears. Such a construction is in itself forbidden, but arises in patterns. Therefore, it was set that if T is instantiated by A &&, then T && (A && &&) = A &&, and in other cases (links to the link) the type is A &, that is:
T = A& => T& = A& T = A& => T&& = A& T = A&& => T& = A& T = A&& => T&& = A&&
Thus, an argument of type T &&, where T is a template type, can accept both types of references. Therefore, a link like T && is called a universal link .
Now it is clear that std :: move can apply T && (see foo (T &&) in the diagram above). Let's add to the pattern of our my_move:
template<class T> T&& my_move(T&& t) { return static_cast<T&&>(t); }
This is an erroneous implementation, since T && turns into A & for lvalue, and, in this case, the instance of the function will be:
A& my_move(A& t) { return static_cast<A&>(t); }
The argument type is correct, but for the rest of the places we needed to leave A && instead of A &. It's time to watch the implementation of std :: move:
template<class T> typename remove_reference<T>::type&& move(T&& _Arg) { return (static_cast<typename remove_reference<T>::type&&>(_Arg)); }
I think it is clear: remove_reference removes all references, and then && is added to the resulting type. By the way, the remove_reference class is very simple. These are the three specializations of the template class with the parameters T, T &, T &&.
template<class T> struct remove_reference { typedef T type; }; template<class T> struct remove_reference<T&> { typedef T type; }; template<class T> struct remove_reference<T&&> { typedef T type; };
It would seem, to deal with all the problems, but no. We have learned to pass on all expressions using the T && pattern, but so far we have not been interested in how to work with them further. The std :: move function does not count, because without thinking, it all leads to an rvalue. Why do we need to distinguish between categories of expressions? Imagine that we are writing an analogue of make_shared
. I remind you that make_shared<T>(...)
itself calls the constructor T with the given arguments. Now is not the time to deal with the variadic templates, therefore, for simplicity, we assume that there is only one argument. We will also return the most common pointer. Take a simple class:
class A { public: A(); // A(const A&); // A(A&&); // };
We want to do this:
A a; A* a1 = make_raw_ptr<A>(a); // A* a2 = make_raw_ptr<A>(A()); // delete a1; delete a2;
Using the compression of the links, we write the implementation:
template<class T, class Arg> T* make_raw_ptr(Arg &&arg) { return new T(arg); };
Well, it works, of course. Only copying always happens. Found a bug? It's simple - arg is always lvalue. It seems that we have lost information about the category of the original expression. Not really - it can still be extracted from Arg. After all, for lvalue: Arg = A &, for rvalue: Arg = A. It turns out that we need a function that restores the type of the link, that is:
A ( ):
// 1- A& my_forward(A& a) { return a; } // 2- A&& my_forward(A& a) // , lvalue { return static_cast<A&&>(a); //, rvalue }
, . , A&, A. , A& — A&, A — A&&. , "" &&.
template<class T> T&& my_forward(T& a) { return static_cast<T&&>(a); } template<class T, class Arg> T* make_raw_ptr(Arg &&arg) { return new T(my_forward<Arg>(arg)); };
std::forward . & &&, .
/ , , . move-. . GCC -fno-elide-constructors
. , .
.
T foo() { T result; return result; } foo();
foo . , . , , :
T temp = result; //
, , . temp — , . , result, , rvalue, foo. temp . , ( & &&) .
, :
T x = foo();
:
T temp = result; // T x = std::move(temp); //
. , .
, ( , sizeof, ), .
C++ : ? std::vector<int> get();
void get(std::vector<int> &);
move- . copy elision — , . , return value optimization (RVO). ( ), , . , , . RVO? Visual Studio 2015:
T foo() { return T(); } class T { public: T() = default; //RVO T() {}; // };
, . , , . std::shared_ptr std::unique_ptr.
Source: https://habr.com/ru/post/322132/
All Articles