1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184
|
In bf(C++), temporary (rvalue) values are indistinguishable from tt(const &)
types. bf(C++) introduces a new reference type called an
emi(rvalue reference), which is defined as ti(typename &&).
The name em(rvalue) reference is derived from assignment statements, where the
variable to the left of the assignment operator is called an emi(lvalue) and
the expression to the right of the assignment operator is called an
emi(rvalue). Rvalues are often temporary, anonymous values, like values
returned by functions.
In this parlance the bf(C++) reference should be considered an
emi(lvalue reference) (using the notation tt(typename &)). They can be
contrasted to em(rvalue references) (using the notation tt(typename &&)).
The key to understanding rvalue references is the concept of an
emi(anonymous variable). An anonymous variable has no name and this is the
distinguishing feature for the compiler to associate it automatically with an
rvalue reference if it has a choice. Before introducing some interesting
constructions let's first have a look at some standard situations where
em(lvalue) references are used. The following function returns a temporary
(anonymous) value:
verb(
int intVal()
{
return 5;
}
)
Although tt(intVal)'s return value can be assigned to an tt(int)
variable it requires copying, which might become prohibitive when
a function does not return an tt(int) but instead some large object. A
em(reference) or em(pointer) cannot be used either to collect the anonymous
return value as the return value won't survive beyond that. So the following
is illegal (as noted by the compiler):
verb(
int &ir = intVal(); // fails: refers to a temporary
int const &ic = intVal(); // OK: immutable temporary
int *ip = &intVal(); // fails: no lvalue available
)
Apparently it is not possible to modify the temporary returned by
tt(intVal). But now consider these functions:
verb(
void receive(int &value) // note: lvalue reference
{
cout << "int value parameter\n";
}
void receive(int &&value) // note: rvalue reference
{
cout << "int R-value parameter\n";
}
)
and let's call this function from tt(main):
verb(
int main()
{
receive(18);
int value = 5;
receive(value);
receive(intVal());
}
)
This program produces the following output:
verb(
int R-value parameter
int value parameter
int R-value parameter
)
The program's output shows the compiler selecting tt(receive(int &&value))
in all cases where it receives an anonymous tt(int) as its argument. Note that
this includes tt(receive(18)): a value 18 has no name and thus tt(receive(int
&&value)) is called. Internally, it actually uses a temporary variable to
store the 18, as is shown by the following example which modifies tt(receive):
verb(
void receive(int &&value)
{
++value;
cout << "int R-value parameter, now: " << value << '\n';
// displays 19 and 6, respectively.
}
)
Contrasting tt(receive(int &value)) with tt(receive(int &&value)) has
nothing to do with tt(int &value) not being a const reference. If
tt(receive(int const &value)) is used the same results are obtained. Bottom
line: the compiler selects the overloaded function using the rvalue reference
if the function is passed an anonymous value.
The compiler runs into problems if tt(void receive(int &value)) is
replaced by tt(void receive(int value)), though. When confronted with the
choice between a value parameter and a reference parameter (either lvalue or
rvalue) it cannot make a decision and reports an ambiguity. In practical
contexts this is not a problem. Rvalue refences were added to the language in
order to be able to distinguish the two forms of references: named values
(for which lvalue references are used) and anonymous values (for which
rvalue references are used).
It is this distinction that allows the implementation of
emi(move semantics) and emi(perfect forwarding). At this point the concept of
emi(move semantics) cannot yet fully be discussed (but see section ref(MOVE)
for a more thorough discussusion) but it is very well possible to illustrate
the underlying ideas.
Consider the situation where a function returns a tt(struct Data) containing a
pointer to dynamically allocated characters. Moreover, the struct defines a
member function tt(copy(Data const &other)) that takes another tt(Data) object
and copies the other's data into the current object. The (partial) definition
of the tt(struct Data) might look like this+footnote(To the observant reader:
in this example the memory leak that results from using Data::copy()
should be ignored):
verb(
struct Data
{
char *text;
size_t size;
void copy(Data const &other)
{
text = strdup(other.text);
size = strlen(text);
}
};
)
Next, functions tt(dataFactory) and tt(main) are defined as follows:
verb(
Data dataFactory(char const *txt)
{
Data ret = {strdup(txt), strlen(txt)};
return ret;
}
int main()
{
Data d1 = {strdup("hello"), strlen("hello")};
Data d2;
d2.copy(d1); // 1 (see text)
Data d3;
d3.copy(dataFactory("hello")); // 2
}
)
At (1) tt(d2) appropriately receives a copy of tt(d1)'s text. But at (2)
tt(d3) receives a copy of the text stored in the temporary returned by the
tt(dataFactory) function. As the temporary ceases to exist after the call to
tt(copy()) two releated and unpleasant consequences are observed:
itemization(
it() The return value is a temporary object: its only reason for
existence is to pass its data on to tt(d3). Now tt(d3) copies the
temporary's data which clearly is somewhat overdone.
it() The temporary tt(Data) object is lost following the call to
tt(copy()). Unfortunately its dynamically allocated data is lost as well
resulting in a memory leak.
)
In cases like these em(rvalue reference) should be used. By overloading
the tt(copy) member with a member tt(copy(Data &&other)) the compiler is able
to distinguish situations (1) and (2). It now calls the initial tt(copy())
member in situation (1) and the newly defined overloaded tt(copy()) member in
situation (2):
verb(
struct Data
{
char *text;
size_t size;
void copy(Data const &other)
{
text = strdup(other.text);
}
void copy(Data &&other)
{
text = other.text;
other.text = 0;
}
};
)
Note that the overloaded tt(copy()) function merely moves the
tt(other.text) pointer to the current object's tt(text) pointer followed by
reassigning 0 to tt(other.text). tt(Struct Data) suddenly has become
emi(move-aware) and implements em(move semantics), removing the drawbacks of
the previously shown approach:
itemization(
it() Instead of making a deep copy (which is required in situation (1)),
the pointer value is simply moved to its new owner;
it() Since the tt(other.text) doesn't point to dynamically allocated
memory anymore the memory leak is prevented.
)
|