Back to blog

Phase 3: Tính Năng Modern C++ (C++11 đến C++23)

cppc++modern-cppprogrammingcpp20
Phase 3: Tính Năng Modern C++ (C++11 đến C++23)

Chào Mừng đến Phase 3

Bạn đã xây dựng nền tảng vững chắc về C++ fundamentals và OOP. Bây giờ là lúc khám phá điều làm cho Modern C++ thực sự hiện đại. Tiêu chuẩn C++11 là một bước ngoặt—biến đổi đến mức Bjarne Stroustrup (người tạo ra C++) mô tả nó như cảm giác một ngôn ngữ hoàn toàn mới. Mỗi bản sửa đổi lớn kể từ đó đã tiếp tục sự phát triển đó.

Phase này khảo sát các tính năng chính được giới thiệu từ C++11 đến C++23: những tính năng phân biệt C++ hiện đại theo phong cách idiomatic với C++ kiểu cũ phong cách C.

Thời gian học: 2-3 tuần, 1-2 giờ mỗi ngày
Điều kiện tiên quyết: Phase 2: Lập Trình Hướng Đối Tượng

Những Gì Bạn Sẽ Học

Kết thúc Phase 3, bạn sẽ có thể:

✅ Viết lambdas biểu cảm và sử dụng chúng với các thuật toán STL
✅ Sử dụng auto, structured bindings và range-for loops một cách thành thạo
✅ Thay thế raw pointers bằng smart pointers
✅ Áp dụng các tiện ích C++17: optional, variant, string_view
✅ Viết constrained templates với C++20 Concepts
✅ Sử dụng Ranges library cho các pipeline có thể kết hợp
✅ Đọc và hiểu std::expected và std::print của C++23


1. Tính Năng Cốt Lõi C++11

Suy Diễn Kiểu auto

auto cho phép compiler suy diễn kiểu từ initializer:

#include <vector>
#include <map>
 
int main() {
    auto x = 42;              // int
    auto pi = 3.14;           // double
    auto name = std::string{"Alice"};  // std::string
 
    std::vector<int> nums = {1, 2, 3, 4, 5};
    auto it = nums.begin();   // std::vector<int>::iterator
 
    std::map<std::string, int> scores;
    // Không dùng auto: std::map<std::string, int>::iterator
    auto mit = scores.begin();
}

Khi nào dùng auto:

  • Kiểu iterator/template dài (luôn rõ ràng hơn)
  • Biến trong vòng lặp range-for
  • Kiểu trả về của lambda
  • Tránh dùng cho kiểu đơn giản khi kiểu có thể cung cấp thông tin (int count = 0)

auto và references:

std::vector<std::string> names = {"Alice", "Bob"};
 
// XẤU: sao chép chuỗi
for (auto name : names) { }
 
// TỐT: reference (không sao chép)
for (auto& name : names) { }
 
// TỐT: const reference (chỉ đọc)
for (const auto& name : names) { }

Vòng Lặp Range-Based For

Lặp sạch hơn trên bất kỳ range nào:

#include <iostream>
#include <vector>
#include <map>
 
int main() {
    std::vector<int> nums = {1, 2, 3, 4, 5};
 
    // Theo value (sao chép)
    for (auto n : nums) {
        std::cout << n << ' ';
    }
 
    // Theo const reference (không sao chép, chỉ đọc)
    for (const auto& n : nums) {
        std::cout << n << ' ';
    }
 
    // Theo reference (sửa đổi tại chỗ)
    for (auto& n : nums) {
        n *= 2;
    }
 
    // Cũng hoạt động với maps
    std::map<std::string, int> ages = {{"Alice", 30}, {"Bob", 25}};
    for (const auto& [name, age] : ages) {  // structured binding!
        std::cout << name << ": " << age << '\n';
    }
}

Lambda Expressions

Lambdas là các hàm ẩn danh được định nghĩa inline:

#include <algorithm>
#include <vector>
#include <iostream>
 
int main() {
    // Cú pháp lambda cơ bản: [capture](params) { body }
    auto greet = [](const std::string& name) {
        std::cout << "Xin chào, " << name << "!\n";
    };
    greet("World");
 
    // Lambda với giá trị trả về
    auto add = [](int a, int b) -> int {
        return a + b;
    };
    std::cout << add(3, 4) << '\n';  // 7
 
    // Sử dụng lambdas với thuật toán STL
    std::vector<int> nums = {5, 2, 8, 1, 9, 3};
 
    // Sắp xếp giảm dần
    std::sort(nums.begin(), nums.end(), [](int a, int b) {
        return a > b;
    });
 
    // Tìm số chẵn đầu tiên
    auto it = std::find_if(nums.begin(), nums.end(), [](int n) {
        return n % 2 == 0;
    });
 
    if (it != nums.end()) {
        std::cout << "Số chẵn đầu tiên: " << *it << '\n';
    }
}

Các chế độ capture:

int x = 10;
int y = 20;
 
// Capture theo value (sao chép x tại thời điểm định nghĩa)
auto f1 = [x]() { return x; };
 
// Capture theo reference
auto f2 = [&x]() { x += 5; };
 
// Capture tất cả theo value
auto f3 = [=]() { return x + y; };
 
// Capture tất cả theo reference
auto f4 = [&]() { x += y; };
 
// Hỗn hợp: x theo value, y theo reference
auto f5 = [x, &y]() { return x + y; };
 
// Capture this (bên trong method của class)
auto f6 = [this]() { return this->member_; };

Mutable lambdas (sửa đổi biến được capture-by-value):

int count = 0;
 
auto counter = [count]() mutable {
    return ++count;  // Sửa đổi BẢN SAO của count
};
 
counter();  // trả về 1
counter();  // trả về 2
// count vẫn là 0 (bản gốc)

nullptr

Thay thế NULL cũ (chỉ là 0):

// Cách cũ (mơ hồ: 0 là int hay pointer?)
int* p1 = NULL;
 
// Cách hiện đại (hằng số null pointer rõ ràng)
int* p2 = nullptr;
 
void foo(int x);
void foo(int* p);
 
// foo(NULL);    // Mơ hồ! Có thể gọi cả hai overload
foo(nullptr);    // Luôn gọi foo(int*)

constexpr

Tính toán giá trị tại thời điểm compile:

// Hàm thông thường: tính tại runtime
int square(int n) { return n * n; }
 
// Hàm constexpr: có thể tính tại compile time
constexpr int square_ce(int n) { return n * n; }
 
int main() {
    constexpr int size = square_ce(10);  // Được đánh giá tại compile time!
    int arr[size];  // OK: size là hằng số compile-time
 
    // Cũng hoạt động tại runtime (linh hoạt)
    int n;
    std::cin >> n;
    int result = square_ce(n);  // Tính tại runtime khi n không phải constexpr
}

constexpr classes (C++14+):

class Point {
public:
    constexpr Point(double x, double y) : x_{x}, y_{y} {}
    constexpr double x() const { return x_; }
    constexpr double y() const { return y_; }
    constexpr double distance_from_origin() const {
        return x_ * x_ + y_ * y_;  // Không sqrt để giữ constexpr
    }
private:
    double x_, y_;
};
 
constexpr Point origin{0.0, 0.0};
constexpr Point p{3.0, 4.0};
constexpr double dist = p.distance_from_origin();  // 25.0 tại compile time

Uniform Initialization

Khởi tạo bằng dấu ngoặc nhọn hoạt động ở mọi nơi, ngăn narrowing:

// Tất cả đây là "uniform initialization"
int x{42};
double pi{3.14};
std::string s{"hello"};
 
struct Point {
    int x, y;
};
Point p{1, 2};  // Aggregate initialization
 
std::vector<int> v{1, 2, 3, 4, 5};  // Initializer list
 
// Ngăn narrowing conversions (lỗi compile)
int y = 3.14;   // OK: âm thầm cắt thành 3
int z{3.14};    // LỖI: narrowing conversion không được phép!

std::initializer_list

Nhận một danh sách dấu ngoặc nhọn làm tham số:

#include <initializer_list>
#include <algorithm>
 
class Stats {
    std::vector<double> data_;
public:
    Stats(std::initializer_list<double> values)
        : data_{values} {}
 
    double max() const {
        return *std::max_element(data_.begin(), data_.end());
    }
    double min() const {
        return *std::min_element(data_.begin(), data_.end());
    }
};
 
Stats s{1.5, 2.3, 0.7, 4.1, 3.0};
std::cout << s.max() << '\n';  // 4.1

enum class (Scoped Enumerations)

Enums strongly-typed không làm ô nhiễm phạm vi bao quanh:

// Enum kiểu cũ (ô nhiễm scope, chuyển đổi int ngầm)
enum Color { RED, GREEN, BLUE };
enum Traffic { RED, YELLOW, GREEN };  // LỖI: RED xung đột!
 
// Scoped enum hiện đại
enum class Color { Red, Green, Blue };
enum class Traffic { Red, Yellow, Green };  // OK!
 
Color c = Color::Red;
Traffic t = Traffic::Red;
 
// Không có chuyển đổi ngầm sang int
int x = c;           // LỖI
int y = static_cast<int>(c);  // OK
 
// Kiểm soát kiểu nền
enum class Flags : uint8_t {
    None  = 0,
    Read  = 1 << 0,
    Write = 1 << 1,
    Exec  = 1 << 2,
};

2. Smart Pointers C++11

Smart pointers là sự thay thế hiện đại cho new/delete thô. Chúng cung cấp quản lý bộ nhớ tự động thông qua RAII.

std::unique_ptr

Quyền sở hữu độc quyền. Khi unique_ptr bị hủy, đối tượng bị xóa.

#include <memory>
 
class Widget {
public:
    Widget(int id) : id_{id} {
        std::cout << "Widget " << id_ << " được tạo\n";
    }
    ~Widget() {
        std::cout << "Widget " << id_ << " bị hủy\n";
    }
    int id() const { return id_; }
private:
    int id_;
};
 
int main() {
    // Tạo unique_ptr (ưu tiên make_unique)
    auto w1 = std::make_unique<Widget>(1);
    std::cout << "w1 id: " << w1->id() << '\n';
 
    // Không thể sao chép, nhưng có thể move
    // auto w2 = w1;           // LỖI: không thể sao chép
    auto w2 = std::move(w1);   // OK: w1 bây giờ là null
 
    if (!w1) {
        std::cout << "w1 rỗng sau khi move\n";
    }
 
    // w2 bị hủy ở đây (Widget 1 bị hủy)
    // w1 là null, không có gì để hủy
}

Pattern factory function:

std::unique_ptr<Shape> makeShape(const std::string& type) {
    if (type == "circle") return std::make_unique<Circle>(5.0);
    if (type == "rect")   return std::make_unique<Rectangle>(3.0, 4.0);
    return nullptr;
}
 
auto shape = makeShape("circle");
if (shape) {
    shape->draw();
}

std::shared_ptr

Quyền sở hữu chung. Đếm tham chiếu: bị hủy khi chủ sở hữu cuối cùng giải phóng.

#include <memory>
 
int main() {
    auto sp1 = std::make_shared<Widget>(42);
    std::cout << "ref count: " << sp1.use_count() << '\n';  // 1
 
    {
        auto sp2 = sp1;  // SAO CHÉP: quyền sở hữu chung
        std::cout << "ref count: " << sp1.use_count() << '\n';  // 2
 
        auto sp3 = sp1;
        std::cout << "ref count: " << sp1.use_count() << '\n';  // 3
    }  // sp2, sp3 bị hủy ở đây
 
    std::cout << "ref count: " << sp1.use_count() << '\n';  // 1
    // Widget bị hủy khi sp1 ra ngoài scope
}

Khi nào dùng cái nào:

Smart PointerDùng Khi
unique_ptrMột chủ sở hữu (đại đa số trường hợp)
shared_ptrQuyền sở hữu thực sự chung (nhiều chủ sở hữu)
weak_ptrPhá vòng tròn tham chiếu với shared_ptr

std::weak_ptr

Tham chiếu không sở hữu đến đối tượng được quản lý bởi shared_ptr. Phá vòng tròn:

#include <memory>
 
class Node {
public:
    std::shared_ptr<Node> next;
    std::weak_ptr<Node> prev;  // weak để phá vòng!
    int value;
 
    Node(int v) : value{v} {}
};
 
int main() {
    auto n1 = std::make_shared<Node>(1);
    auto n2 = std::make_shared<Node>(2);
 
    n1->next = n2;
    n2->prev = n1;  // weak_ptr, không tăng ref count
 
    // Dùng weak_ptr: phải lock() để lấy shared_ptr tạm thời
    if (auto locked = n2->prev.lock()) {
        std::cout << "Giá trị n1: " << locked->value << '\n';
    }
}

3. Tính Năng C++14

C++14 là bản phát hành tinh chỉnh—đánh bóng và mở rộng C++11.

Generic Lambdas

Dùng tham số auto trong lambdas (lambda templates):

// C++11: phải chỉ định kiểu
auto add11 = [](int a, int b) { return a + b; };
 
// C++14: generic lambda với tham số auto
auto add14 = [](auto a, auto b) { return a + b; };
 
std::cout << add14(1, 2) << '\n';        // int: 3
std::cout << add14(1.5, 2.5) << '\n';   // double: 4.0
std::cout << add14(std::string{"hi"}, std::string{" there"}) << '\n';

Suy Diễn Kiểu Trả Về

Các hàm có thể suy diễn kiểu trả về từ các câu lệnh return:

// C++11: phải chỉ định kiểu trả về
auto sum11(int a, int b) -> int { return a + b; }
 
// C++14: suy diễn từ câu lệnh return
auto sum14(int a, int b) { return a + b; }  // suy diễn int
 
// Tất cả đường dẫn return phải trả về cùng kiểu
auto classify(int n) {
    if (n > 0) return std::string{"dương"};
    if (n < 0) return std::string{"âm"};
    return std::string{"không"};
}

std::make_unique

C++11 có std::make_shared nhưng quên std::make_unique. Được sửa trong C++14:

// C++11: phải tự viết
auto p = std::unique_ptr<Widget>(new Widget(42));
 
// C++14: sạch và exception-safe
auto p = std::make_unique<Widget>(42);

Binary Literals và Digit Separators

// Binary literals (C++14)
int flags = 0b1010'1100;  // 172
 
// Digit separators để dễ đọc
long long million = 1'000'000;
double pi = 3.141'592'653'589'793;
unsigned hex = 0xFF'FF'FF'FF;

Thuộc Tính [[deprecated]]

Đánh dấu hàm/kiểu là deprecated với thông báo tùy chọn:

[[deprecated("Sử dụng newFunction() thay thế")]]
void oldFunction() {
    // ...
}
 
void newFunction() {
    // ...
}
 
int main() {
    oldFunction();  // Cảnh báo compiler: deprecated
    newFunction();  // OK
}

4. Tính Năng C++17

C++17 mang lại nhiều tính năng thực tế cao đơn giản hóa code hàng ngày.

Structured Bindings

Giải nén tuples, pairs và structs thành các biến có tên:

#include <map>
#include <tuple>
#include <string>
 
int main() {
    // Giải nén pair
    std::pair<std::string, int> p{"Alice", 30};
    auto [name, age] = p;
    std::cout << name << " " << age << " tuổi\n";
 
    // Giải nén tuple
    auto t = std::make_tuple(1, 2.5, std::string{"hello"});
    auto [i, d, s] = t;
 
    // Giải nén struct (aggregate)
    struct Point { int x, y; };
    Point pt{10, 20};
    auto [x, y] = pt;
 
    // Lặp map (rất hữu ích!)
    std::map<std::string, int> scores = {{"Alice", 95}, {"Bob", 87}};
    for (const auto& [student, score] : scores) {
        std::cout << student << ": " << score << '\n';
    }
 
    // Giải nén kết quả insert
    auto [it, inserted] = scores.insert({"Charlie", 92});
    if (inserted) {
        std::cout << "Đã thêm: " << it->first << '\n';
    }
}

if và switch với Initializers

Khởi tạo biến có phạm vi trong câu lệnh if:

#include <map>
#include <string>
 
std::map<std::string, int> data = {{"key", 42}};
 
// C++17: init-statement trong if
if (auto it = data.find("key"); it != data.end()) {
    std::cout << "Tìm thấy: " << it->second << '\n';
}
// 'it' không truy cập được ở đây
 
// Hữu ích cho mutex guards
if (std::lock_guard lock{mutex}; queue.empty()) {
    // ...
}
 
// Cũng hoạt động với switch
switch (auto ch = getChar(); ch) {
    case 'a': // ...
    case 'b': // ...
    default: break;
}

std::optional

Đại diện cho một giá trị có thể có hoặc không. Thay thế sentinel values và nullptr:

#include <optional>
#include <string>
 
// Trả về optional thay vì dùng -1, null, hoặc exceptions
std::optional<int> parseInt(const std::string& s) {
    try {
        return std::stoi(s);
    } catch (...) {
        return std::nullopt;  // Không có giá trị
    }
}
 
int main() {
    auto result = parseInt("42");
    if (result) {
        std::cout << "Đã phân tích: " << *result << '\n';
    }
 
    // value_or cung cấp giá trị mặc định
    auto bad = parseInt("không phải số");
    std::cout << bad.value_or(-1) << '\n';  // -1
 
    // Optional trong chữ ký hàm (mục đích rõ hơn)
    std::optional<std::string> findUser(int id);
 
    if (auto user = findUser(42)) {
        std::cout << "Người dùng: " << *user << '\n';
    }
}

std::variant

Union an toàn kiểu. Giữ đúng một trong số nhiều kiểu có thể:

#include <variant>
#include <string>
#include <iostream>
 
// Giá trị có thể là int, double hoặc string
using Value = std::variant<int, double, std::string>;
 
void printValue(const Value& v) {
    // std::visit dispatch dựa trên kiểu đang hoạt động
    std::visit([](const auto& val) {
        std::cout << val << '\n';
    }, v);
}
 
int main() {
    Value v1 = 42;
    Value v2 = 3.14;
    Value v3 = std::string{"hello"};
 
    printValue(v1);  // 42
    printValue(v2);  // 3.14
    printValue(v3);  // hello
 
    // Kiểm tra kiểu đang hoạt động
    if (std::holds_alternative<int>(v1)) {
        std::cout << "int: " << std::get<int>(v1) << '\n';
    }
 
    // Phong cách pattern matching với overloaded visitor
    struct Visitor {
        void operator()(int n)                { std::cout << "int: " << n << '\n'; }
        void operator()(double d)             { std::cout << "double: " << d << '\n'; }
        void operator()(const std::string& s) { std::cout << "string: " << s << '\n'; }
    };
 
    std::visit(Visitor{}, v3);
}

Variant như xử lý lỗi:

using Result = std::variant<std::string, std::error_code>;
 
Result readFile(const std::string& path) {
    // Nếu thành công: trả về nội dung
    // Nếu lỗi: trả về error code
}

std::string_view

View không sở hữu vào một chuỗi. Không phân bổ, không sao chép:

#include <string_view>
#include <string>
 
// Cách cũ: luôn sao chép
void processOld(const std::string& s);
 
// Tốt hơn: không phân bổ cho string literals
void processNew(std::string_view sv) {
    std::cout << sv.size() << ": " << sv << '\n';
    // sv.substr() trả về string_view (không phân bổ!)
    auto first5 = sv.substr(0, 5);
}
 
int main() {
    processNew("hello world");       // Không phân bổ
    processNew(std::string{"hi"});   // Không sao chép
    const char* c = "raw string";
    processNew(c);                   // Hoạt động!
 
    // Các thao tác string_view (không phân bổ)
    std::string_view sv = "Xin chào, Thế Giới!";
    auto pos = sv.find("Thế");
    if (pos != std::string_view::npos) {
        std::cout << sv.substr(pos) << '\n';
    }
}

Cảnh báo: string_view không được tồn tại lâu hơn chuỗi mà nó tham chiếu.

[[nodiscard]], [[fallthrough]], [[maybe_unused]]

Các thuộc tính tiêu chuẩn cho cảnh báo tốt hơn:

// [[nodiscard]]: cảnh báo nếu giá trị trả về bị bỏ qua
[[nodiscard]] int computeResult() { return 42; }
 
// [[nodiscard]] với thông báo (C++20)
[[nodiscard("Kiểm tra error code!")]]
std::error_code openFile(const std::string& path);
 
// [[fallthrough]]: fallthrough switch có chủ ý
switch (n) {
    case 1:
        doSomething();
        [[fallthrough]];  // Có chủ ý!
    case 2:
        doMore();
        break;
}
 
// [[maybe_unused]]: tắt cảnh báo biến không dùng
[[maybe_unused]] int debug_flag = 1;
 
void function([[maybe_unused]] int param) {
    // param không dùng trong release build
    #ifdef DEBUG
    std::cout << param << '\n';
    #endif
}

std::filesystem

Các thao tác filesystem portable:

#include <filesystem>
#include <iostream>
 
namespace fs = std::filesystem;
 
int main() {
    fs::path p = "/home/user/documents";
 
    // Thao tác path
    std::cout << p.filename() << '\n';   // "documents"
    std::cout << p.parent_path() << '\n'; // "/home/user"
    std::cout << p.extension() << '\n';   // "" (không có extension)
 
    fs::path file = p / "report.txt";   // nối path
    std::cout << file << '\n';           // "/home/user/documents/report.txt"
 
    // Lặp thư mục
    for (const auto& entry : fs::directory_iterator(".")) {
        if (entry.is_regular_file()) {
            std::cout << entry.path().filename() << " ("
                      << entry.file_size() << " bytes)\n";
        }
    }
 
    // Thao tác file
    fs::create_directories("output/data");  // Tạo thư mục lồng nhau
    fs::copy("source.txt", "dest.txt");
    fs::remove("old_file.txt");
 
    // Kiểm tra tồn tại
    if (fs::exists("config.json")) {
        std::cout << "Tìm thấy config\n";
    }
}

Fold Expressions

Áp dụng toán tử nhị phân cho các variadic template parameter packs:

// Tổng của bất kỳ số lượng đối số nào
template<typename... Args>
auto sum(Args... args) {
    return (args + ...);  // Unary right fold
    // Mở rộng thành: args1 + (args2 + (args3 + ...))
}
 
template<typename... Args>
bool allTrue(Args... args) {
    return (... && args);  // Unary left fold
}
 
template<typename T, typename... Args>
void print(T first, Args... rest) {
    std::cout << first;
    ((std::cout << ", " << rest), ...);  // Fold trên toán tử dấu phẩy
    std::cout << '\n';
}
 
int main() {
    std::cout << sum(1, 2, 3, 4, 5) << '\n';  // 15
    std::cout << allTrue(true, true, false) << '\n';  // false
    print(1, "hello", 3.14, 'x');  // 1, hello, 3.14, x
}

5. Tính Năng C++20

C++20 là bản cập nhật C++ lớn nhất kể từ C++11, thêm bốn tính năng chính (Concepts, Ranges, Coroutines, Modules) cùng nhiều tính năng nhỏ hơn.

Concepts

Ràng buộc tham số kiểu template với các yêu cầu được đặt tên:

#include <concepts>
 
// Định nghĩa concept: T phải hỗ trợ < và ==
template<typename T>
concept Comparable = requires(T a, T b) {
    { a < b } -> std::convertible_to<bool>;
    { a == b } -> std::convertible_to<bool>;
};
 
// Dùng concept để ràng buộc template
template<Comparable T>
T max(T a, T b) {
    return (a < b) ? b : a;
}
 
// Các concepts thư viện tiêu chuẩn
template<std::integral T>
T factorial(T n) {
    if (n <= 1) return 1;
    return n * factorial(n - 1);
}
 
template<std::floating_point T>
T divide(T a, T b) {
    return a / b;
}
 
// Abbreviated function templates (cú pháp ngắn gọn)
auto add(std::integral auto a, std::integral auto b) {
    return a + b;
}

Các concepts tiêu chuẩn có sẵn:

// <concepts>
std::same_as<T, U>      // T và U là cùng kiểu
std::derived_from<T, U> // T được dẫn xuất từ U
std::convertible_to<T, U>
std::integral<T>        // T là kiểu integer
std::floating_point<T>  // T là kiểu dấu phẩy động
std::copyable<T>        // T có thể sao chép
std::movable<T>         // T có thể move
std::invocable<F, Args> // F có thể gọi với Args

Thông báo lỗi được cải thiện: Với concepts, compiler nói "T does not satisfy Comparable" thay vì 50 dòng lỗi template substitution.

Ranges Library

Các thuật toán range có thể kết hợp và lazy:

#include <ranges>
#include <vector>
#include <iostream>
 
int main() {
    std::vector<int> nums = {1, 2, 3, 4, 5, 6, 7, 8, 9, 10};
 
    // Kết hợp views với | (toán tử pipe)
    auto result = nums
        | std::views::filter([](int n) { return n % 2 == 0; }) // số chẵn
        | std::views::transform([](int n) { return n * n; })    // bình phương
        | std::views::take(3);                                  // 3 đầu tiên
 
    for (auto n : result) {
        std::cout << n << ' ';  // 4 16 36
    }
    std::cout << '\n';
 
    // Các thuật toán Ranges (không cần begin/end)
    std::vector<int> data = {5, 2, 8, 1, 9};
    std::ranges::sort(data);  // [1, 2, 5, 8, 9]
 
    auto found = std::ranges::find(data, 5);
    if (found != data.end()) {
        std::cout << "Tìm thấy: " << *found << '\n';
    }
}

Các views hữu ích:

namespace views = std::views;
 
// Tạo ranges
auto zero_to_9 = views::iota(0, 10);        // 0, 1, ..., 9
auto naturals = views::iota(1);             // 1, 2, 3, ... (vô hạn)
 
// Transform views
auto uppercase = str | views::transform(::toupper);
auto split_words = text | views::split(' ');
 
// Kết hợp
auto first5evens = views::iota(1)
    | views::filter([](int n) { return n % 2 == 0; })
    | views::take(5);
// 2, 4, 6, 8, 10 — được đánh giá lazily!

Three-Way Comparison (<=>)

"Spaceship operator" tạo ra tất cả các toán tử so sánh:

#include <compare>
 
struct Version {
    int major, minor, patch;
 
    // Định nghĩa <=> và == thu được <, <=, >, >= miễn phí
    auto operator<=>(const Version&) const = default;
    bool operator==(const Version&) const = default;
};
 
int main() {
    Version v1{1, 2, 3};
    Version v2{1, 3, 0};
 
    std::cout << (v1 < v2) << '\n';   // true
    std::cout << (v1 > v2) << '\n';   // false
    std::cout << (v1 == v2) << '\n';  // false
    std::cout << (v1 <= v2) << '\n';  // true
}

std::span

View không sở hữu trên một dãy liên tục (array, vector, raw array):

#include <span>
#include <vector>
#include <iostream>
 
// Cũ: truyền pointer + size (dễ lỗi)
void printOld(const int* data, size_t size) {
    for (size_t i = 0; i < size; ++i)
        std::cout << data[i] << ' ';
}
 
// Hiện đại: span (an toàn, hoạt động với mọi container liên tục)
void printModern(std::span<const int> data) {
    for (const auto& n : data) {
        std::cout << n << ' ';
    }
    std::cout << '\n';
}
 
int main() {
    std::vector<int> vec = {1, 2, 3, 4, 5};
    int arr[] = {6, 7, 8, 9, 10};
 
    printModern(vec);           // Hoạt động với vector
    printModern(arr);           // Hoạt động với mảng C
    printModern({arr, 3});      // 3 phần tử đầu tiên
    printModern(vec);           // Toàn bộ vector
 
    // Subviews của Span (không phân bổ)
    std::span<int> s{vec};
    auto first3 = s.first(3);   // [1, 2, 3]
    auto last2  = s.last(2);    // [4, 5]
    auto middle = s.subspan(1, 3);  // [2, 3, 4]
}

std::format

Định dạng chuỗi kiểu Python (thay thế printf và stringstream):

#include <format>
#include <string>
 
int main() {
    // Định dạng cơ bản
    std::string s = std::format("Xin chào, {}!", "Thế Giới");
    std::cout << s << '\n';  // Xin chào, Thế Giới!
 
    // Đối số theo vị trí
    auto msg = std::format("{0} + {1} = {2}", 3, 4, 3 + 4);
    std::cout << msg << '\n';  // 3 + 4 = 7
 
    // Format specifiers (tương tự printf)
    std::cout << std::format("{:>10}", "phải") << '\n';   // căn phải rộng 10
    std::cout << std::format("{:<10}", "trái") << '\n';   // căn trái
    std::cout << std::format("{:^10}", "giữa") << '\n';   // căn giữa
    std::cout << std::format("{:.3f}", 3.14159) << '\n';  // 3.142
    std::cout << std::format("{:08x}", 255) << '\n';      // 000000ff
    std::cout << std::format("{:,}", 1234567) << '\n';    // 1,234,567
}

Designated Initializers

Khởi tạo các thành viên aggregate theo tên:

struct Config {
    int port = 8080;
    std::string host = "localhost";
    bool ssl = false;
    int timeout = 30;
};
 
// Cũ: theo vị trí (dễ lỗi, mong manh)
Config c1{9090, "example.com", true, 60};
 
// C++20: designated initializers (rõ ràng)
Config c2{
    .port = 9090,
    .host = "example.com",
    .ssl = true,
    .timeout = 60,
};
 
// Chỉ chỉ định những gì cần; phần còn lại lấy mặc định
Config c3{
    .port = 3000,
    .ssl = true,
};

consteval và constinit

// consteval: PHẢI được đánh giá tại compile time (không giống constexpr có thể runtime)
consteval int square(int n) {
    return n * n;
}
 
constexpr int a = square(5);  // OK: compile time
// int x; square(x);          // LỖI: x không phải constexpr
 
// constinit: biến được khởi tạo tại compile time, nhưng không nhất thiết là const
constinit int counter = 0;  // Được khởi tạo tại compile time
// counter = 5;              // OK: không phải const, có thể sửa đổi

Coroutines (Cơ Bản)

Coroutines là các hàm có thể tạm dừng và tiếp tục. C++20 cung cấp cơ chế; các thư viện như cppcoro cung cấp wrappers tiện lợi:

#include <coroutine>
#include <generator>  // C++23, hoặc dùng cppcoro cho C++20
 
// Generator coroutine: tạo ra các giá trị lazily
std::generator<int> fibonacci() {
    int a = 0, b = 1;
    while (true) {
        co_yield a;       // Tạm dừng và yield giá trị
        auto next = a + b;
        a = b;
        b = next;
    }
}
 
int main() {
    auto fibs = fibonacci();
    for (int i = 0; i < 10; ++i) {
        auto it = fibs.begin();
        std::cout << *it << ' ';
        ++it;
    }
}

Ba từ khóa coroutine:

  • co_yield: Tạm dừng và tạo ra một giá trị
  • co_return: Kết thúc coroutine (tùy chọn trả về giá trị)
  • co_await: Tạm dừng cho đến khi một awaitable hoàn thành (dùng trong async I/O)

6. Tính Năng C++23

C++23 tiếp tục tinh chỉnh ngôn ngữ với các bổ sung thực tế.

std::expected

Xử lý lỗi tốt hơn exceptions hoặc error codes. Giống optional, nhưng mang giá trị lỗi khi rỗng:

#include <expected>
#include <string>
 
enum class ParseError { InvalidInput, Overflow, Empty };
 
std::expected<int, ParseError> parsePositive(std::string_view s) {
    if (s.empty()) return std::unexpected(ParseError::Empty);
 
    int result = 0;
    for (char c : s) {
        if (c < '0' || c > '9')
            return std::unexpected(ParseError::InvalidInput);
        result = result * 10 + (c - '0');
        if (result < 0)
            return std::unexpected(ParseError::Overflow);
    }
    return result;
}
 
int main() {
    auto result = parsePositive("42");
    if (result) {
        std::cout << "Đã phân tích: " << *result << '\n';
    } else {
        switch (result.error()) {
            case ParseError::InvalidInput: std::cout << "Đầu vào không hợp lệ\n"; break;
            case ParseError::Empty: std::cout << "Đầu vào rỗng\n"; break;
            case ParseError::Overflow: std::cout << "Tràn số\n"; break;
        }
    }
 
    // value_or cho giá trị mặc định
    std::cout << parsePositive("bad").value_or(-1) << '\n';  // -1
 
    // Monadic operations
    auto doubled = parsePositive("21")
        .transform([](int n) { return n * 2; })
        .value_or(0);
    std::cout << doubled << '\n';  // 42
}

std::print và std::println

In trực tiếp, type-safe mà không cần std::cout:

#include <print>
 
int main() {
    std::print("Xin chào, {}!\n", "Thế Giới");   // Không có newline sau
    std::println("Xin chào, {}!", "Thế Giới");   // Tự động thêm newline
 
    std::println("{} + {} = {}", 1, 2, 3);
    std::println("{:.2f}", 3.14159);              // 3.14
 
    // In ra stderr
    std::println(stderr, "Lỗi: {}", "có gì đó sai");
}

Deducing this

Tham số this tường minh cho phép polymorphism không cần CRTP và fluent builders:

struct Builder {
    std::string name;
    int age = 0;
 
    // 'this' là tường minh, cho phép perfect forwarding
    template<typename Self>
    auto&& setName(this Self&& self, std::string n) {
        self.name = std::move(n);
        return std::forward<Self>(self);
    }
 
    template<typename Self>
    auto&& setAge(this Self&& self, int a) {
        self.age = a;
        return std::forward<Self>(self);
    }
};
 
Builder b;
b.setName("Alice").setAge(30);

std::flat_map và std::flat_set

Các containers được sắp xếp, liên tục. Hiệu suất cache tốt hơn std::map dựa trên cây:

#include <flat_map>
#include <flat_set>
 
int main() {
    // Giống std::map, nhưng được hỗ trợ bởi sorted vector
    std::flat_map<std::string, int> scores;
    scores["Alice"] = 95;
    scores["Bob"] = 87;
    // Được lưu trong bộ nhớ liên tục được sắp xếp — lặp nhanh hơn
 
    // Giống std::set, nhưng được hỗ trợ bởi sorted vector
    std::flat_set<int> primes{2, 3, 5, 7, 11};
    primes.insert(13);
    // Tra cứu O(log n) nhưng cache behavior tốt hơn std::set
}

if consteval

Phát hiện tại compile time xem bạn có đang trong ngữ cảnh constant-evaluated hay không:

constexpr double computePi() {
    if consteval {
        // Đường dẫn compile-time (có thể dùng thao tác chỉ compile-time)
        return 3.14159265358979323846L;
    } else {
        // Đường dẫn runtime
        return std::numbers::pi;
    }
}

7. So Sánh Tính Năng Qua Các Tiêu Chuẩn

// Nhiệm vụ: Lọc số chẵn, bình phương chúng, thu thập vào vector
 
// Phong cách C++03
std::vector<int> result_03;
for (size_t i = 0; i < nums.size(); ++i) {
    if (nums[i] % 2 == 0) {
        result_03.push_back(nums[i] * nums[i]);
    }
}
 
// Phong cách C++11
std::vector<int> result_11;
std::copy_if(nums.begin(), nums.end(), std::back_inserter(result_11),
    [](int n) { return n % 2 == 0; });
std::transform(result_11.begin(), result_11.end(), result_11.begin(),
    [](int n) { return n * n; });
 
// Phong cách C++20 (ranges)
auto result_20 = nums
    | std::views::filter([](int n) { return n % 2 == 0; })
    | std::views::transform([](int n) { return n * n; })
    | std::ranges::to<std::vector>();

8. Những Gì Cần Áp Dụng Trước

Không phải tất cả tính năng đều cần sự chú ý bằng nhau. Đây là thứ tự áp dụng thực tế:

Áp dụng ngay lập tức (C++11/14):

Tính NăngTại Sao
autoGiảm verbose, tránh type mismatches
Range-forLặp sạch hơn, ít lỗi hơn
Smart pointersLoại bỏ delete và memory leaks
LambdasThay thế function objects và bind
nullptrNull type-safe
override/finalPhát hiện lỗi overriding tại compile time
enum classEnums có phạm vi, type-safe
make_unique/make_sharedPhân bổ exception-safe

Áp dụng khi cần (C++17):

Tính NăngTại Sao
Structured bindingsLặp pair/tuple/map sạch hơn
std::optionalThay thế null returns và sentinels
std::string_viewTham số chuỗi zero-cost
[[nodiscard]]Ngăn bỏ qua error returns
if với initializerGiới hạn phạm vi biến

Học và dùng có chọn lọc (C++20):

Tính NăngTại Sao
ConceptsLỗi template tốt hơn, ràng buộc rõ ràng hơn
RangesPipeline dữ liệu có thể kết hợp, dễ đọc
std::formatThay thế printf và stringstream
Designated initializersXây dựng struct rõ ràng hơn
<=>Tự động các toán tử so sánh

Các Lỗi Phổ Biến

1. Dangling string_view

// NGUY HIỂM: string_view tồn tại lâu hơn chuỗi
std::string_view sv;
{
    std::string temp = "hello";
    sv = temp;  // OK ở đây
}  // temp bị hủy!
// sv bây giờ dangling — undefined behavior khi dùng

Giải pháp: Đừng lưu trữ string_view nếu chuỗi có thể bị hủy.

2. Capture Local By Reference trong Long-Lived Lambdas

std::function<int()> make_counter_bad() {
    int count = 0;
    return [&count]() { return ++count; };  // DANGLING: count đã biến mất!
}
 
std::function<int()> make_counter_good() {
    int count = 0;
    return [count]() mutable { return ++count; };  // OK: sao chép!
}

3. Dùng shared_ptr Ở Mọi Nơi

// XẤU: shared_ptr khi chỉ có một chủ sở hữu
auto sp = std::make_shared<Widget>(42);
 
// TỐT: unique_ptr cho quyền sở hữu đơn
auto up = std::make_unique<Widget>(42);

shared_ptr có overhead (reference counting, control block). Dùng unique_ptr theo mặc định.

4. Bỏ Qua [[nodiscard]]

[[nodiscard]] std::error_code writeFile(const std::string& path, const std::string& data);
 
// CẢNH BÁO: kết quả bị bỏ qua
writeFile("output.txt", "content");
 
// ĐÚNG: kiểm tra kết quả
if (auto err = writeFile("output.txt", "content")) {
    std::cerr << "Ghi thất bại: " << err.message() << '\n';
}

Tóm Tắt

✅ C++11: auto, range-for, lambdas, smart pointers, nullptr, constexpr, enum class
✅ C++14: Generic lambdas, suy diễn kiểu trả về, make_unique, digit separators
✅ C++17: Structured bindings, optional, variant, string_view, filesystem, [[nodiscard]]
✅ C++20: Concepts, Ranges, format, span, spaceship operator, designated initializers
✅ C++23: std::expected, std::print, deducing this, flat_map/flat_set

Tiếp Theo Là Gì?

Tài Nguyên Bổ Sung


Series: Lộ Trình Học Modern C++
Trước: Phase 2: Lập Trình Hướng Đối Tượng
Tiếp theo: Deep Dive: Quản Lý Bộ Nhớ & Smart Pointers

📬 Đăng ký nhận bản tin

Nhận những bài viết mới nhất qua email mỗi tuần. Không spam, hủy đăng ký bất cứ lúc nào.

Chúng tôi tôn trọng quyền riêng tư của bạn. Hủy đăng ký bất cứ lúc nào.

💬 Comments

Đăng nhập để bình luận

Chúng tôi sẽ không đăng bất cứ điều gì mà không có sự cho phép của bạn.