Code for the BufferOwner
class is here: http://coliru.stacked-crooked.com/a/0b21daacd5dff5b4
Any expression in a given scope which is associated with addressable memory that is valid in that scope.
// Example 1:
{
int x;
foo(x); // x is an lvalue expression
x += 2; // x is an lvalue expression
int *p = &x; // &x is not an lvalue expression.
// You cannot say &&x to get its address.
int arr[10];
a[0] = 5; // a[0] is an lvalue expression.
}
// Example 2:
class Foo {
public:
Foo(int n) : x_(n) {}
int& X() {
return x_;
}
int* pX() {
return &x_;
}
private:
int x_;
};
int main() {
Foo f(10);
std::cout << f.X() << '\n'; // prints 10
f.X() = 20;
std::cout << f.X() << '\n'; // prints 20.
// f.X() is an lvalue expression as
// it returns an addressable reference.
std::cout << &(f.X()) << '\n'; // prints address of member x_ in f.
std::cout << *f.pX() << '\n'; // f.pX() is not an lvalue expression
// std::cout << &f.pX() << '\n'; // as you cannot do this.
}
Any expression that is not an lvalue expression.
// Example 1.
int main() {
std::cout << std::string("Hello") << '\n'; // std::string("Hello") is an rvalue expression.
// std::cout << &std::string("Hello") << '\n'; // you cannot write &std::string("Hello")
}
// Example 2.
std::string hello() {
return "hello";
}
int main() {
std::cout << hello() << '\n'; // hello() is an rvalue-expression
// std::cout << &hello() << '\n'; // &hello() is not valid.
}
Why is this important?
- If an expression has an associated address that is valid throughout a scope, then the compiler cannot move it by default. It can only copy it.
- But if an expression is known to not be associated with an address that is valid throughout a scope, then no one can access that address in that scope after the expression. So the compiler can decide to move from such an expression by default.
- If we want to move from an lvalue expression, we must explicitly tell the compiler that it is ok to move from this expression. This is what std::move does.
- Thus: the compiler can always move from an rvalue. In fact, the correct definition of rvalue is: "An expression whose content can be moved by default by the compiler".
Thus:
BufferOwner buf{...};
BufferOwner buf1{...};
buf = buf1; // will invoke copy assignment
buf1 = BufferOwner{"I'm a temporary rvalue"}; // will invoke move because the source is an rvalue.
BufferOwner buf2{buf1}; // will invoke copy because the source is an lvalue.
BufferOwner buf3 = std::move(buf1); // will invoke move, because buf1 is lvalue *but* std::move(buf1) allows moving.
Therefore, std::move(expr) converts an lvalue expression (expr) to an rvalue expression.
But there is a contradiction. Let us take a function:
void foo(T&& obj) {
...
std::cout << "Address of obj is " << &obj << '\n';
}
T obj;
foo(std::move(obj));
In the above, we are taking the address of x, which is rvalue. This is not allowed by definition. Actually, an expression of type T&& is not an rvalue. It is an x-value - a crossover of an lvalue (whose address can be taken) and an rvalue (which can be moved by default).
Thus:
BufferOwner getBuffer(const char* text) {
BufferOwner b{"hello"};
return b;
}
BufferOwner b1{getBuffer("hello")}; // will invoke move constructor of b1 because getBuffer("hello") is rvalue.
void foo(BufferOwner b) {
std::cout << b.get() << '\n';
}
int main() {
BufferOwner b1{"Hi"};
foo(std::move(b1)}; // caller initiated move
}
void foo(BufferOwner&& b) {
std::cout << b.get() << '\n';
// callee initiated move
// commenting the below would mean, no move takes place
BufferOwner localCopy(std::move(b));
}
int main() {
BufferOwner b1{"Hi"};
foo(std::move(b1)}; // callee allowed move.
}
int main() {
foo(BufferOwner{"Hi"}}; // will always move, as BufferOwner{"Hi"} is rvalue.
}
TBD