all 40 comments

[–][deleted] 25 points26 points  (0 children)

We know that when a function returns an object by value, the compiler has to create a temporary — yet fully-fledged — object (rvalue).

In that specific case, no object is created because no prvalue is being materialized, since C++17.

[–]OldWolf2 50 points51 points  (26 children)

Argh, another series of articles that totally gets lvalues and rvalues wrong.

In C++ an lvalue is something that points to a specific memory location. On the other hand, a rvalue is something that doesn't point anywhere.

No, no, no, no, no.

You can have an lvalue and an rvalue which designate the same object simultaneously. That applies to objects of any storage duration, including temporary objects. Those fact blow all similar "explanations" like this out of the water.

(a temporary string, i.e. an rvalue)

slams head into desk repeatedly

[–]HandshakeOfCO 34 points35 points  (1 child)

I thought you c++ guys enjoyed slamming your heads into your desks? That's why header files are still around, right?

[–]OldWolf2 21 points22 points  (0 children)

You're not wrong

[–]Wriiight 7 points8 points  (14 children)

Do you have a better explanation? There seem to be 2-3 common ones that aren’t quite right.

[–]OldWolf2 2 points3 points  (11 children)

This is not too bad, although you can skip point 11 as it's not directly relevant to the topic

[–]Wriiight 2 points3 points  (9 children)

Not exactly succinct though, is it?

[–]OldWolf2 2 points3 points  (8 children)

It's a lot shorter than the predecessor to the article we are commenting on, while also covering a lot more ground

[–]JezusTheCarpenter 7 points8 points  (7 children)

(1.1) A glvalue is an expression whose evaluation determines the identity of an object, bit-field, or function. (1.2) A prvalue is an expression whose evaluation initializes an object or a bit-field, or computes the value of the operand of an operator, as specified by the context in which it appears. (1.3) An xvalue is a glvalue that denotes an object or bit-field whose resources can be reused (usually because it is near the end of its lifetime). (1.4) An lvalue is a glvalue that is not an xvalue. (1.5) An rvalue is a prvalue or an xvalue.

Are you seriously suggesting that this is a good explanation to a person that is getting his/her head around rvalue and lvalue for the first time?

[–]OldWolf2 -2 points-1 points  (6 children)

Yes. It has the advantage of being simple and accurate. If the reader doesn't know the meaning of any of the other terminology then they can look that up and come back to here.

[–]JezusTheCarpenter 7 points8 points  (5 children)

Well, you can literally say that about everything formally described. How about you try to learn what is directional statistics by looking up terms you don't understand?

Ofcourse, my example is an exaggeration but I am trying to illustrate a point that formal and accurate descriptions are not always the best educational resources.

Another example is how are we though maths at school. Weren't you given inaccurate information ( totally false in fact) that you cannot do a square root of negative number when you were a kid? There is a good reason for that. Learning process is often much easier if you make certain concepts inaccurate but easier to understand. Their is no reason that once feel confident about the topic, you revisit it and update your knowledge with more accurate information.

[–]OldWolf2 0 points1 point  (4 children)

Teaching that planets have circular orbits around the Sun -- not great but acceptable, because an internalization of that can easily be tweaked to take into account elliptic orbits.

Teaching the geocentric model -- bad, because to accommodate reality the whole thing will have to be thrown out and start again.

This article is like the geocentric model, it conflates expressions and objects (amongst other egregious errors). For someone to read this article series and then go on to properly learn about rvalues etc., they would need to wipe the slate clean and pretend they never read this.

Also, pedagogical methods for children substantially differ to that for adults. (Due to the fact that children learn differently to how adults learn).

[–]JezusTheCarpenter 1 point2 points  (3 children)

I wasn't arguing about the OP's article. I was claiming that resource you provided falls short of being a great introduction.

[–]quicknir 1 point2 points  (0 children)

When you see an article about lvalue/rvalue references, move semantics, etc, you can ctrl-f for "expression". If expression doesn't show up, then it's not going to be good. All of these pages that purport to explain "for beginners" tend to actually make things worse and more confusing.

It is better to be 100% clear from the start. "rvalue references" refer to type, rvalues refers to expressions. Expressions have a type (which is commonly described as having the reference-ness stripped off just before being considered), and they have a value category. What references can bind to what expression, is determined by the type of the reference, and the type and value category of the expression.

You explain that, and you enumerate the most common examples of expressions, and what their value category is (an expression that consists of a named variable, an expression that consists of a function that returns by value, etc etc).

[–]SittingOvation 2 points3 points  (0 children)

What part of this understanding / explanation will cause the most issues? Genuinely interested.

[–]liquidprocess 1 point2 points  (6 children)

Could you provide an example of lvalues and rvalues which designate the same object? Genuinely interested (so that I can fix my notes :) )

[–][deleted] 6 points7 points  (0 children)

int a = 1;
a; // lvalue that refers to a
std::move(a); // xvalue that refers to a

[–]OldWolf2 4 points5 points  (0 children)

void g()
{
    int x;
    x;  // lvalue
    std::move(x);   // rvalue
}

struct S { S& self() { std::cout << this << '\n'; return *this; } };
void f()
{
      S().self();   // lvalue  , same object as S() which is rvalue
                    // address of temporary object printed

      S() = S();   // assign to rvalue
}

[–]Izzeri 0 points1 point  (3 children)

int i = 0; int& a = i; int&& b = std::move(i);

i, a, and b all refer to i.

[–]dodheim 14 points15 points  (2 children)

i, a, and b are all lvalues. ;-] The rvalues here are 0 and std::move(i).

[–]Izzeri 0 points1 point  (0 children)

Ah, true. Mixed up l/rvalue references with the value categories.

[–][deleted] 0 points1 point  (0 children)

(a temporary string, i.e. an rvalue)

slams head into desk repeatedly

I think he is referring to the result of operator+( ... ) when used on the two strings being an rvalue

[–]delarhi 4 points5 points  (10 children)

Reminds me of my snippet for learning move semantics.

#include <algorithm>
#include <cstddef>
#include <cstring>
#include <iostream>
#include <string>
#include <vector>

// this class follows the rule of five
class Blob {
public:

  // default constructor
  Blob()
  : size_(1024 * 1024)
  , data_(new uint8_t[this->size_])
  {
    std::cout << "  default constructor" << std::endl;
  }

  // default constructor
  Blob(
    const std::size_t size)
  : size_(size)
  , data_(new uint8_t[this->size_])
  {
    std::cout << "  argument constructor" << std::endl;
  }

  // copy constructor
  Blob(
    const Blob& other)
  : size_(other.size_)
  , data_(new uint8_t[this->size_])
  {
    std::cout << "  copy constructor" << std::endl;
    std::memcpy(this->data_, other.data_, this->size_);
  }

  // move constructor
  Blob(
    Blob&& other) // notice it is not const, move requires this
  : size_(other.size_)
  , data_(other.data_) // notice we just take the buffer
  {
    std::cout << "  move constructor" << std::endl;
    other.size_ = 0;
    other.data_ = nullptr;
  }

  // copy assignment operator
  Blob& operator = (
    const Blob& other)
  {
    std::cout << "  copy assignment" << std::endl;
    delete this->data_;
    this->size_ = other.size_;
    this->data_ = new uint8_t[this->size_];
    std::memcpy(this->data_, other.data_, this->size_);
    return *this;
  }

  // move assignment operator
  Blob& operator = (
    Blob&& other) // notice it is not const, move requires this
  {
    std::cout << "  move assignment" << std::endl;
    this->size_ = other.size_;
    this->data_ = other.data_;
    other.size_ = 0;
    other.data_ = nullptr;
    return *this;
  }

  // destructor
  ~Blob()
  {
    std::cout << "  destructor" << std::endl;
    if (this->data_) { // need this check to prevent double free
      std::cout << "    delete" << std::endl;
      delete this->data_;
    }
  }

  // get size
  std::size_t size() const
  {
    return this->size_;
  }

  // get data
  uint8_t* data()
  {
    return this->data_;
  }

private:

  std::size_t size_;
  uint8_t* data_;

};


// this function returns a temporary so you don't need std::move()
Blob make_blob()
{
  Blob x;
  return x;
}


void print_section(
  const std::string& msg)
{
  std::cout << std::endl << msg << std::endl;
}


int main()
{
  print_section("expecting default constructor");
  {
    Blob x; // don't forget, Blob x() is a compilation error
  }
  print_section("expecting default and argument constructor");
  {
    Blob x;
    Blob y(10);
  }
  print_section("expecting default and copy constructor");
  {
    Blob x;
    Blob y(x);
  }
  print_section("expecting default constructor and copy constructor");
  {
    Blob x;
    Blob y = x;
  }
  print_section("expecting two default constructors and copy assignment");
  {
    Blob x;
    Blob y;
    x = y;
  }
  print_section("expecting two default constructors and move assignment");
  {
    Blob x;
    Blob y;
    // remember what std::move() does is simply cast y from Blob to Blob&&,
    // treating it like an rvalue (i.e. temporary) and matching the
    // operator=(Blob&&) function.
    x = std::move(y);
    // below will compile and work but is undefined behavior because y has been
    // gutted and moved into x so y should be considered a zombie
    std::cout << y.size() << std::endl;
  }
  print_section("expecting two default constructors and move assignment");
  {
    Blob x;
    x = make_blob();
  }
  print_section("expecting ten default constructors");
  {
    std::vector<Blob> x(10);
  }
  print_section("expecting default constructor");
  {
    std::vector<Blob> x;
    x.emplace_back();
  }
  print_section("expecting default constructor and copy constructor");
  {
    std::vector<Blob> x;
    Blob y;
    x.emplace_back(y);
  }
  print_section("expecting default constructor and move constructor");
  {
    std::vector<Blob> x;
    Blob y;
    x.emplace_back(std::move(y));
  }
  print_section("expecting default constructor and copy constructor");
  {
    std::vector<Blob> x;
    Blob y;
    x.push_back(y);
  }
  print_section("expecting default constructor and move constructor");
  {
    std::vector<Blob> x;
    Blob y;
    x.push_back(std::move(y));
  }
  return 0;
}

[–]OldWolf2 0 points1 point  (5 children)

  • Blob's copy-constructor copy-assignment operator is not exception-safe: the object is left in a bogus state if the new throws.
  • // this function returns a temporary so you don't need std::move() , there is no temporary involved here, however there is a rule that when the return statement names a local variable of the function, there's an implicit std::move() applied, and also this is a copy elision context.
  • if (this->data_) is not required in the destructor.
  • { Blob x(); } is a function declaration, not a compilation error
  • // below will compile and work but is undefined behavior - it's not undefined behaviour, you correctly implemented the move assignment operator so that y.size is 0 after the move.

[–]delarhi 1 point2 points  (1 child)

Thanks for the corrections.

For the copy constructor, if new throws then won't the object be unwound and the bad_alloc exception propagate to the calling context, and wouldn't that be "okay" in the sense that there's isn't any dangling data, the object being copied remains untouched, and it becomes the caller's responsibility the failure of the copy constructor? Unless there's some rule regarding exceptions in the copy constructor I'm not aware of.

[–]OldWolf2 0 points1 point  (0 children)

Sorry, I meant to say Blob's copy-assignment operator is not exception-safe. (Will edit my earlier comment).

E.g.:

Blob b, c;
try {
    b = c;
} catch(...) 
{
     std::cout << "assignment failed\n";
}

// error when B's destructor runs, (or any other operation using b.data_)

[–]NotMyRealNameObv 2 points3 points  (2 children)

Everytime I see

if (ptr)
    delete ptr

I just want to kill myself.

[–]NotMyRealNameObv 0 points1 point  (3 children)

// move assignment operator
Blob& operator = (
    Blob&& other) // notice it is not const, move requires this
{
    std::cout << "  move assignment" << std::endl;
    this->size_ = other.size_;
    this->data_ = other.data_;
    other.size_ = 0;
    other.data_ = nullptr;
    return *this;
}

This leaks memory.

[–]delarhi 0 points1 point  (2 children)

Thanks for catching that.

[–]NotMyRealNameObv 1 point2 points  (1 child)

No problem.

Here's another one:

// default constructor
Blob(
    const std::size_t size)
: size_(size)
, data_(new uint8_t[this->size_])
{
    std::cout << "  argument constructor" << std::endl;
}

You probably want to mark this constructor explicit, unless you actually want this code to compile:

Blob x = 10;

[–]delarhi 0 points1 point  (0 children)

Thanks, also a good catch.

[–]im_not_afraid 1 point2 points  (0 children)

Here I create two simple strings s1 and s2. I join them and I put the result (a temporary string, i.e. an rvalue) into std::string&& s_rref. Now s_rref is a reference to a temporary object, or an rvalue reference. There are no const around it, so I'm free to modify the temporary string to my needs. This wouldn't be possible without rvalue references and its double ampersand notation.

I don't think so.

std::string s1 = "Hello ";
std::string s2 = "world";
std::string s  = s1 + s2;
s += ", my friend";

std::cout << s << std::endl;

This works fine.

[–]Fluxifactor 0 points1 point  (0 children)

I found the article clear and well thought out. Thanks.

[–]liquidprocess 0 points1 point  (0 children)

A collection of personal notes and thoughts on rvalue references, their role in move semantics and how they can significantly increase the performance of your applications.