13.6.3. Rvalue References and Member Functions

Image

Member functions other than constructors and assignment can benefit from providing both copy and move versions. Such move-enabled members typically use the same parameter pattern as the copy/move constructor and the assignment operators—one version takes an lvalue reference to const, and the second takes an rvalue reference to nonconst.

For example, the library containers that define push_back provide two versions: one that has an rvalue reference parameter and the other a const lvalue reference. Assuming X is the element type, these containers define:

void push_back(const X&); // copy: binds to any kind of X
void push_back(X&&);      // move: binds only to modifiable rvalues of type X

We can pass any object that can be converted to type X to the first version of push_back. This version copies data from its parameter. We can pass only an rvalue that is not const to the second version. This version is an exact match (and a better match) for nonconst rvalues and will be run when we pass a modifiable rvalue (§ 13.6.2, p. 539). This version is free to steal resources from its parameter.

Ordinarily, there is no need to define versions of the operation that take a const X&& or a (plain) X&. Usually, we pass an rvalue reference when we want to “steal” from the argument. In order to do so, the argument must not be const. Similarly, copying from an object should not change the object being copied. As a result, there is usually no need to define a version that take a (plain) X& parameter.


Image Note

Overloaded functions that distinguish between moving and copying a parameter typically have one version that takes a const T& and one that takes a T&&.


As a more concrete example, we’ll give our StrVec class a second version of push_back:

class StrVec {
public:
    void push_back(const std::string&);  // copy the element
    void push_back(std::string&&);       // move the element
    // other members as before
};
// unchanged from the original version in § 13.5 (p. 527)
void StrVec::push_back(const string& s)
{
    chk_n_alloc(); // ensure that there is room for another element
    // construct a copy of s in the element to which first_free points
    alloc.construct(first_free++, s);
}
void StrVec::push_back(string &&s)
{
    chk_n_alloc(); // reallocates the StrVec if necessary
    alloc.construct(first_free++, std::move(s));
}

These members are nearly identical. The difference is that the rvalue reference version of push_back calls move to pass its parameter to construct. As we’ve seen, the construct function uses the type of its second and subsequent arguments to determine which constructor to use. Because move returns an rvalue reference, the type of the argument to construct is string&&. Therefore, the string move constructor will be used to construct a new last element.

When we call push_back the type of the argument determines whether the new element is copied or moved into the container:

StrVec vec;  // empty StrVec
string s = "some string or another";
vec.push_back(s);      // calls push_back(const string&)
vec.push_back("done"); // calls push_back(string&&)

These calls differ as to whether the argument is an lvalue (s) or an rvalue (the temporary string created from "done"). The calls are resolved accordingly.

Rvalue and Lvalue Reference Member Functions

Ordinarily, we can call a member function on an object, regardless of whether that object is an lvalue or an rvalue. For example:

string s1 = "a value", s2 = "another";
auto n = (s1 + s2).find('a'),

Here, we called the find member (§ 9.5.3, p. 364) on the string rvalue that results from adding two strings. Sometimes such usage can be surprising:

s1 + s2 = "wow!";

Here we assign to the rvalue result of concatentating these strings.

Prior to the new standard, there was no way to prevent such usage. In order to maintain backward compatability, the library classes continue to allow assignment to rvalues, However, we might want to prevent such usage in our own classes. In this case, we’d like to force the left-hand operand (i.e., the object to which this points) to be an lvalue.

Image

We indicate the lvalue/rvalue property of this in the same way that we define const member functions (§ 7.1.2, p. 258); we place a reference qualifier after the parameter list:

class Foo {
public:
    Foo &operator=(const Foo&) &; // may assign only to modifiable lvalues
    // other members of Foo
};
Foo &Foo::operator=(const Foo &rhs) &
{
    // do whatever is needed to assign rhs to this object
    return *this;
}

The reference qualifier can be either & or &&, indicating that this may point to an rvalue or lvalue, respectively. Like the const qualifier, a reference qualifier may appear only on a (nonstatic) member function and must appear in both the declaration and definition of the function.

We may run a function qualified by & only on an lvalue and may run a function qualified by && only on an rvalue:

Foo &retFoo();  // returns a reference; a call to retFoo is an lvalue
Foo retVal();   // returns by value; a call to retVal is an rvalue
Foo i, j;       // i and j are lvalues
i = j;          // ok: i is an lvalue
retFoo() = j;   // ok: retFoo() returns an lvalue
retVal() = j;   // error: retVal() returns an rvalue
i = retVal();   // ok: we can pass an rvalue as the right-hand operand to assignment

A function can be both const and reference qualified. In such cases, the reference qualifier must follow the const qualifier:

class Foo {
public:
    Foo someMem() & const;    // error: const qualifier must come first
    Foo anotherMem() const &; // ok: const qualifier comes first
};

Overloading and Reference Functions

Just as we can overload a member function based on whether it is const7.3.2, p. 276), we can also overload a function based on its reference qualifier. Moreover, we may overload a function by its reference qualifier and by whether it is a const member. As an example, we’ll give Foo a vector member and a function named sorted that returns a copy of the Foo object in which the vector is sorted:

class Foo {
public:
    Foo sorted() &&;         // may run on modifiable rvalues
    Foo sorted() const &;    // may run on any kind of Foo
    // other members of Foo
private:
    vector<int> data;
};
// this object is an rvalue, so we can sort in place
Foo Foo::sorted() &&
{
    sort(data.begin(), data.end());
    return *this;
}
// this object is either const or it is an lvalue; either way we can't sort in place
Foo Foo::sorted() const & {
    Foo ret(*this);                         // make a copy
    sort(ret.data.begin(), ret.data.end()); // sort the copy
    return ret;                             // return the copy
}

When we run sorted on an rvalue, it is safe to sort the data member directly. The object is an rvalue, which means it has no other users, so we can change the object itself. When we run sorted on a const rvalue or on an lvalue, we can’t change this object, so we copy data before sorting it.

Overload resolution uses the lvalue/rvalue property of the object that calls sorted to determine which version is used:

retVal().sorted(); // retVal() is an rvalue, calls Foo::sorted() &&
retFoo().sorted(); // retFoo() is an lvalue, calls Foo::sorted() const &

When we define const memeber functions, we can define two versions that differ only in that one is const qualified and the other is not. There is no similar default for reference qualified functions. When we define two or more members that have the same name and the same parameter list, we must provide a reference qualifier on all or none of those functions:

class Foo {
public:
    Foo sorted() &&;
    Foo sorted() const; // error: must have reference qualifier
    // Comp is type alias for the function type (see § 6.7 (p. 249))
    // that can be used to compare int values
    using Comp = bool(const int&, const int&);
    Foo sorted(Comp*);        // ok: different parameter list
    Foo sorted(Comp*) const;  // ok: neither version is reference qualified
};

Here the declaration of the const version of sorted that has no parameters is an error. There is a second version of sorted that has no parameters and that function has a reference qualifier, so the const version of that function must have a reference qualifier as well. On the other hand, the versions of sorted that take a pointer to a comparison operation are fine, because neither function has a qualifier.


Image Note

If a member function has a reference qualifier, all the versions of that member with the same parameter list must have reference qualifiers.



Exercises Section 13.6.3

Exercise 13.55: Add an rvalue reference version of push_back to your StrBlob.

Exercise 13.56: What would happen if we defined sorted as:


Foo Foo::sorted() const & {
    Foo ret(*this);
    return ret.sorted();
}

Exercise 13.57: What if we defined sorted as:


Foo Foo::sorted() const & { return Foo(*this).sorted(); }

Exercise 13.58: Write versions of class Foo with print statements in their sorted functions to test your answers to the previous two exercises.


..................Content has been hidden....................

You can't read the all page of ebook, please click here login for view all page.
Reset
3.146.34.146