References, const correctness and value categories

Tell lvalues from rvalues, use std::move and std::forward correctly, and design interfaces that refuse to copy unnecessarily.

References and value categories

#include <utility>
#include <string>
#include <vector>

std::string name = "ada";
std::string&       lref = name;          // lvalue reference: binds to a named object
const std::string& cref = name;          // read-only view, can bind to a temporary
std::string&&      rref = std::move(name); // rvalue reference: an xvalue

// lvalue  = has an identity, you can take its address
// rvalue  = a temporary, on the right of an assignment
// xvalue  = an object you have promised not to reuse (result of std::move)

void take_by_value(std::string s);       // copies in, absorbs rvalues cheaply
void take_by_cref(const std::string& s); // never copies, never mutates
void take_by_rref(std::string&& s);      // takes ownership of the argument
ParameterCopiesUse when
T valueOne move or copyYou will store it, or T is cheap (int, pointer, string_view)
const T&NoneYou only read; the default for non-trivial types
T&NoneYou must modify the caller's object
T&&NoneA move constructor or a sink parameter named t
std::span<T>NoneA contiguous range you only read

std::move does not move anything. It is a cast to an rvalue reference that makes the move constructor eligible. After std::move(x) you may not read x except to assign to it or destroy it.

Forwarding and copy elision

#include <utility>
#include <vector>
#include <string>

template <class T, class... Args>
T make(Args&&... args) {                       // forwarding references
    return T(std::forward<Args>(args)...);     // preserve value category
}

void sink(std::string&& s);                     // forward, do not re-move
template <class T>
void pass(T&& t) {
    sink(std::forward<T>(t));                   // correct for lvalue and rvalue callers
    // sink(std::move(t));                      // wrong: turns every caller argument into an xvalue
}

struct Widget {
    std::string a;
    std::vector<int> b;

    Widget(std::string a_, std::vector<int> b_)         // take by value
        : a(std::move(a_)), b(std::move(b_)) {}         // then move into the member

    Widget(Widget&&) noexcept = default;
    Widget& operator=(Widget&&) noexcept = default;
    Widget(const Widget&) = default;
    Widget& operator=(const Widget&) = default;
};

Widget build() {
    Widget w{"x", {1, 2, 3}};
    return w;                    // NRVO: constructed directly in the caller's storage
}

int main() {
    auto w = build();            // no copy, no move: guaranteed copy elision in C++17
    Widget other = std::move(w); // explicit move; w is now valid but unspecified
}
  • Writing return std::move(local); defeats NRVO and can make the code slower, not faster.
  • After a move, the object is in a valid but unspecified state. Call clear() if you want it reusable; never assume it is empty.
  • Only mark move operations noexcept when they truly are, but do mark them: std::vector reallocation uses move instead of copy only for noexcept move constructors.
  • A moving operation that throws leaves the source in an unknown state, which is why the standard library containers avoid it.
// const correctness as API documentation
class Buffer {
public:
    std::size_t size() const noexcept;                 // does not mutate
    char&       operator[](std::size_t i);             // mutable access
    const char& operator[](std::size_t i) const;       // read-only access
private:
    std::unique_ptr<char[]> data_;
    std::size_t size_ = 0;
};
💡
Mark every member function that does not modify observable state as const. A const method can be called on a const object or through a const reference, which is exactly what template code and the standard algorithms require.

FAQ

Should I pass std::string by value or by const reference?
Take const std::string& when you only read. Take by value when you store a copy anyway — one move for rvalue callers, one copy for lvalue callers — and then std::move it into the member. Do not take by rvalue reference unless you write a move constructor.
What does std::forward do that std::move does not?
std::forward<T> preserves the caller's value category, so an lvalue argument stays an lvalue and an rvalue stays an rvalue. std::move unconditionally casts to an rvalue, which is wrong inside a forwarding function because it makes a copy unavoidable for lvalue callers.

Classes, constructors and RAII Templates, concepts and generic programming

Last refreshed 2026-09-18.