lvalue and rvalue

The names come from which side of an assignment something can legally sit on, but the useful mental model is simpler: an lvalue has an identifiable, persistent address — anything with a name is an lvalue. An rvalue is a temporary that has no address you can rely on past the current expression — the right-hand side of a computation, not "readable" anything (a mnemonic worth retiring if you picked it up elsewhere; it doesn't describe the actual distinction).

int x = 5;        // x is an lvalue — it has a name and an address
"hello";           // still an lvalue — string literals have static storage
x + 2;             // rvalue — a temporary computed value
int *v = &x;
*(v + 2);          // lvalue — dereferencing gives you an addressable location
v + 2;             // rvalue — the pointer arithmetic itself is a temporary

int square(int &x)        { return x * x; }   // binds only lvalues
int square(int &&x)       { return x * x; }   // binds only rvalues
int square(const int &x)  { return x * x; }   // binds both — a const lvalue reference can bind an rvalue
            
A function call is an rvalue only when the function returns by value — that's the temporary case std::move is about. A function that returns by reference (T&) yields an lvalue instead, as the example below shows:

int &foo() { return globalVar; }
foo() = 5;   // legal — sets globalVar to 5, because foo() returns an lvalue reference here, not a plain rvalue
            

std::move

std::move doesn't move anything by itself — it's a cast. It converts its argument to an rvalue reference (static_cast<T&&>(x), literally), which tells the compiler "I no longer need this object's current value here"; that's what makes the move constructor overload eligible instead of the copy constructor. After the move, the moved-from object is left in a valid but unspecified state — still safe to destroy or reassign, just not safe to assume anything about its contents.
std::move only helps when the type actually has something to move. int, double, and other types that live entirely on the stack have no separate heap buffer to steal — "moving" one just copies it, the same as it always did. The optimization is specifically for types like std::string or std::vector that own a heap allocation.

std::vector<std::string> vec1 = {"a", "b", "c"};
std::vector<std::string> vec2;

vec2 = vec1;               // deep copy — every string is duplicated
vec2 = std::move(vec1);    // vec2 now owns vec1's internal buffer directly; vec1 is left empty
            

rule of three, rule of five

Rule of three: if a class needs to define any one of destructor, copy constructor, or copy assignment operator, it almost certainly needs all three — the usual reason is that the class manages a resource (heap memory, a file handle) where the compiler-generated member-by-member copy would produce a shallow copy and a double-free.
Rule of five: C++11 adds move constructor and move assignment to that set. Declaring any of destructor, copy constructor, or copy assignment suppresses the compiler's implicit generation of the move members — so a class holding a resource that could otherwise benefit from moving needs to declare all five explicitly, or it silently falls back to copying every time a move would have been possible.
All five, on one class, with a message in each so you can see which one runs when: TopNotchNote/cpp/move_semantics_rule_of_five.cpp

Animal a;              // default ctor
Animal b("dog", "bark"); // parameterized ctor
Animal c(b);            // copy ctor
Animal d = b;            // ALSO copy ctor — "Animal d = b" is sugar for "Animal d(b)", not an assignment
Animal e;
e = b;                    // default ctor for e, then copy assignment
Animal f(std::move(b));  // move ctor — b is left in a valid-but-empty state after this
            
One easy mistake with the rule of five: it's specifically any user-declared destructor that suppresses the implicit move members, not something special about virtual destructors. A base class with a virtual destructor loses implicit move support for the same reason a base class with a non-virtual one does — because it declared a destructor at all. See Inheritance & Polymorphism for why base classes need a virtual destructor in the first place.

the copy-and-swap idiom

A common way to implement operator= exception-safely and support both copy and move assignment with one function: take the parameter by value (which invokes either the copy or move constructor, whichever applies at the call site), then swap its internals with *this. The temporary parameter is destroyed at the end of the function, taking the old resource with it.

class Buffer {
  int *data;
  size_t size;
public:
  Buffer(size_t n) : data(new int[n]()), size(n) {}
  Buffer(const Buffer &other) : data(new int[other.size]), size(other.size) {
    std::copy(other.data, other.data + size, data);
  }
  Buffer(Buffer &&other) noexcept : data(other.data), size(other.size) {
    other.data = nullptr;   // leave other in a valid, destructible (empty) state
    other.size = 0;
  }
  ~Buffer() { delete[] data; }

  void swap(Buffer &other) noexcept {
    std::swap(data, other.data);
    std::swap(size, other.size);
  }
  Buffer & operator=(Buffer other) {   // note: by value, not by reference
    swap(other);                          // steal other's guts
    return *this;
  }                                        // other (holding our OLD heap buffer) is destroyed here — freeing it
};

Buffer a(4), b(8);
a = b;               // b is copied into the parameter (copy ctor), then swapped in — no manual delete[]/new[] needed
a = std::move(b);     // b is moved into the parameter (move ctor above), then swapped in — b is left empty
a = Buffer(16);       // the temporary is constructed directly in the parameter (guaranteed copy elision,
                       // C++17) — no copy or move constructor call happens here at all
            
Note this class needed an explicit move constructor to make the std::move(b) line above actually move rather than copy: a user-declared copy constructor and destructor (both present here) suppress the compiler's implicit generation of a move constructor, per the rule of five below. Without one, std::move(b) would silently fall back to the copy constructor instead of failing to compile.
One caveat worth knowing rather than fighting: if a class defines both a copy-and-swap operator=(T) and a separate operator=(T&&), calls like b = std::move(a) become ambiguous — the compiler can't tell which one you meant. Pick one strategy: either copy-and-swap alone (simpler, one function, handles both cases via the by-value parameter) or a genuinely separate move-assignment operator (marginally faster, more code to maintain) — not both at once.

move and push_back

push_back is overloaded as push_back(const T&) and push_back(T&&) — by const reference or by rvalue reference, not by value — so pushing a named variable copies it, but pushing a temporary moves it. That's the practical reason to prefer vec.push_back(getData()) over auto tmp = getData(); vec.push_back(tmp); when you don't need the intermediate variable for anything else — the first version lets the compiler move straight from the temporary instead of copying a named lvalue.

std::vector<std::string> names;
std::string s = "reused elsewhere too";
names.push_back(s);                 // copies — s still has its value afterward, and you use it below
names.push_back(getName());          // moves — getName()'s return value is a temporary (rvalue)
names.push_back(std::move(s));       // moves — you're explicitly saying you're done with s
            
One trap: marking a variable const disables moving it, silently. There's no const T&& overload that push_back (or any move constructor) actually uses for mutation, so a const std::string passed to std::move just falls back to the copy overload — it compiles, it's just not actually moving anything.

const std::string s = "won't move";
vec.push_back(std::move(s));   // compiles fine, but this is a copy, not a move — s is const
            

where to go from here

Classes & Constructors — constructors and initialization lists in full.
Smart Pointers — std::move is exactly how unique_ptr ownership transfers.
STL Containers — where push_back's copy/move overloads come from.
Perfect Forwarding & Universal References — what happens to a T&& parameter once template deduction gets involved.

reference

cppreference — std::move
cppreference — move constructors