Chuyển đến nội dung

References: Lvalue, Rvalue & Pitfalls

Explore lvalue and rvalue references, understand C++ value categories, learn reference collapsing rules and forwarding references, and avoid dangling reference bugs.

Lvalue References

An lvalue reference (T&) is an alias — another name for an existing object. Unlike pointers, references must be initialized, cannot be null, and cannot be re-seated to refer to a different object.

Lvalue references bind to lvalues — expressions that have identity (you can take their address). Variables, array elements, and dereferenced pointers are all lvalues. A non-const lvalue reference (int&) cannot bind to a temporary (rvalue) because modifying a temporary is almost always a bug.

Lvalue Reference Basics

Lvalue references are the workhorse of C++ — used for pass-by-reference, operator overloading, and avoiding copies.

lvalue_refs.cpp
#include <iostream>
#include <string>

void append_greeting(std::string& s) {
    s += ", welcome!";  // modifies the caller's string
}

int main() {
    int x = 10;
    int& ref = x;      // ref is an alias for x
    ref = 20;
    std::cout << x << '\n';  // 20 — x was modified through ref

    // const reference can bind to a temporary (rvalue)
    const std::string& greeting = std::string("Hello");
    // The temporary's lifetime is extended to match greeting's scope
    std::cout << greeting << '\n';  // "Hello"

    // Non-const lvalue reference CANNOT bind to rvalue:
    // std::string& bad = std::string("oops");  // COMPILER ERROR

    std::string name = "Alice";
    append_greeting(name);
    std::cout << name << '\n';  // "Alice, welcome!"
}

Danh mục giá trị trong C++

C++ có phân loại chính xác về các loại giá trị biểu thức. Hiểu chúng là điều cần thiết cho ngữ nghĩa di chuyển và chuyển tiếp hoàn hảo:

- lvalue — có danh tính, không thể di chuyển ngầm. Ví dụ: biến,*ptr,arr[i], chuỗi ký tự.
- prvalue (rvalue thuần túy) — không có danh tính, có thể được di chuyển từ đó. Ví dụ:42,x + y,std::string("temp"), hàm trả về theo giá trị.
- xvalue (giá trị hết hạn) — có danh tính VÀ có thể được di chuyển từ đó. Ví dụ:std::move(x),static_cast(x), thành viên của một giá trị.
- glvalue (lvalue tổng quát) — lvalue hoặc xvalue (có danh tính).
- rvalue — prvalue hoặc xvalue (có thể được chuyển từ đó).

Thông tin chi tiết quan trọng: biến tham chiếu rvalue bản thân nó là lvalue — biến này có tên và địa chỉ. Đây là nguồn gốc của nhiều sự nhầm lẫn và chính xác là lý do tại saostd::forwardtồn tại.

Tham chiếu giá trị (T&&)

Tham chiếu giá trị liên kết với giá trị (đối tượng tạm thời vàstd::move'd). Chúng là nền tảng của ngữ nghĩa di chuyển — chúng cho phép bạn phát hiện khi nào một đối tượng sắp bị phá hủy và đánh cắp tài nguyên của nó.

rvalue_refs.cpp
#include <iostream>
#include <string>
#include <utility>  // std::move

void process(const std::string& s) {
    std::cout << "lvalue: " << s << '\n';
}

void process(std::string&& s) {
    std::cout << "rvalue: " << s << '\n';
    // s is an lvalue inside this function (it has a name!)
    // To move from s, you'd need std::move(s)
}

int main() {
    std::string a = "hello";
    process(a);               // calls lvalue overload
    process(std::string("world"));  // calls rvalue overload
    process(std::move(a));    // calls rvalue overload — a is now moved-from

    // IMPORTANT: a is in a valid but unspecified state
    // Only safe operations: assign to it, destroy it
    std::cout << "a after move: '" << a << "'\n";  // likely empty
}

Thu gọn và chuyển tiếp tham chiếu

Khi các tham chiếu kết hợp trong các mẫu, quy tắc thu gọn tham chiếu sẽ được áp dụng:

-T& &thu gọn thànhT&
-T& &&thu gọn thànhT&
-T&& &thu gọn thànhT&
-T&& &&thu gọn thànhT&&

Quy tắc rất đơn giản: nếu một trong hai tham chiếu là tham chiếu giá trị, thì kết quả là tham chiếu giá trị. Chỉ&& &&cung cấp&&.

Tham chiếu chuyển tiếp (còn được gọi là tham chiếu chung) làT&&trong đóTlà tham số mẫu được suy ra. Nó có thể liên kết với cả giá trị và giá trị.auto&&cũng là một tài liệu tham khảo chuyển tiếp. Nhưngstd::vector&&KHÔNG -Tđã được biết đến.const T&&cũng không phải là tài liệu tham khảo chuyển tiếp.

Lỗi thường gặp

Tham chiếu lơ lửng đề cập đến một đối tượng bị phá hủy. Không giống như con trỏ lơ lửng, không có kiểm tranullptr— mã sẽ âm thầm sử dụng dữ liệu rác hoặc bị lỗi.

Nguyên nhân phổ biến:

- Trả về tham chiếu đến biến cục bộ — biến bị hủy khi hàm trả về.
- Chụp bằng tham chiếu trong lambda tồn tại lâu hơn biến được tham chiếu.
- const auto&thành tạm thời bên trong một biểu thức —const auto& val = get_map()["key"]bị treo nếuget_map()trả về theo giá trị, vì bản đồ tạm thời bị hủy ở dấu chấm phẩy.
- Vòng lặp for dựa trên phạm vi tạm thời —for (auto& x : get_vector())vẫn ổn (tồn tại tạm thời cho vòng lặp), nhưngfor (auto& x : get_wrapper().get_vector())có thể treo lủng lẳng nếu trình bao bọc bị phá hủy.

Gia hạn trọn đời với const T&

Tham chiếu giá trịconstcó thể kéo dài thời gian tồn tại tạm thời, nhưng chỉ khi bị ràng buộc trực tiếp. Điều này KHÔNG hoạt động thông qua các lệnh gọi hàm hoặc chuỗi truy cập thành viên.

lifetime_extension.cpp
#include <iostream>
#include <string>

std::string make_greeting() { return "Hello, World!"; }

const std::string& identity(const std::string& s) { return s; }

int main() {
    // SAFE: const ref directly bound to temporary
    const std::string& good = make_greeting();
    std::cout << good << '\n';  // OK — lifetime extended

    // DANGEROUS: lifetime extension does NOT propagate through functions
    const std::string& bad = identity(make_greeting());
    // bad is DANGLING — the temporary was destroyed after identity() returned
    // std::cout << bad << '\n';  // UNDEFINED BEHAVIOR

    // SAFE: just use a value
    std::string safe = make_greeting();  // move or copy-elision
    std::cout << safe << '\n';  // always safe
}
Điểm chính
  • Tham chiếu giá trị (T&) là bí danh - phải được khởi tạo, không thể rỗng hoặc đặt lại
  • const T&có thể liên kết với các phần tạm thời và kéo dài tuổi thọ của chúng (nhưng chỉ khi được liên kết trực tiếp)
  • Tham chiếu giá trị (T&&) liên kết với các giá trị tạm thời và cho phép ngữ nghĩa di chuyển
  • Một tham chiếu giá trị được đặt tên tự nó là một giá trị - điều này rất quan trọng để hiểu
  • Thu gọn tham chiếu: bất kỳ&nào trong chuỗi sẽ tạo ra kết quả&
  • Tham chiếu chuyển tiếp (T&&vớiTđược suy ra) liên kết với cả giá trị và giá trị

Câu đố — Kiểm tra kiến thức

(15 XP)

1. Với`void f(std::string&& s)`, loại giá trị của`s`bên trong`f`là gì?

2. Quy tắc thu gọn tham chiếu nào là đúng?

3. Khi nào phần mở rộng trọn đời`const T&`hoạt động tạm thời?