Skip to content

Instantly share code, notes, and snippets.

@amukherj
Last active November 28, 2019 11:02
Show Gist options
  • Save amukherj/d06c6dbde324e8310649cf005c3441c4 to your computer and use it in GitHub Desktop.
Save amukherj/d06c6dbde324e8310649cf005c3441c4 to your computer and use it in GitHub Desktop.

Expression classes

Code for the BufferOwner class is here: http://coliru.stacked-crooked.com/a/0b21daacd5dff5b4

lvalue expression:

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.
}

rvalue expression: (This is not the precise definition but the good enough definition)

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.

Calling semantics

Caller initiated move (always explicit)

void foo(BufferOwner b) {
  std::cout << b.get() << '\n';
}

int main() {
  BufferOwner b1{"Hi"};
  foo(std::move(b1)};   // caller initiated move
}

Callee initiated move

Explicit 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.
}

Implicit move

int main() {
  foo(BufferOwner{"Hi"}};    // will always move, as BufferOwner{"Hi"} is rvalue.
}

Return value semantics

TBD

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment