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 timeUniform 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.1enum 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 Pointer | Dùng Khi |
|---|---|
unique_ptr | Một chủ sở hữu (đại đa số trường hợp) |
shared_ptr | Quyền sở hữu thực sự chung (nhiều chủ sở hữu) |
weak_ptr | Phá 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 ArgsThô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 đổiCoroutines (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ăng | Tại Sao |
|---|---|
auto | Giảm verbose, tránh type mismatches |
| Range-for | Lặp sạch hơn, ít lỗi hơn |
| Smart pointers | Loại bỏ delete và memory leaks |
| Lambdas | Thay thế function objects và bind |
nullptr | Null type-safe |
override/final | Phát hiện lỗi overriding tại compile time |
enum class | Enums có phạm vi, type-safe |
make_unique/make_shared | Phân bổ exception-safe |
Áp dụng khi cần (C++17):
| Tính Năng | Tại Sao |
|---|---|
| Structured bindings | Lặp pair/tuple/map sạch hơn |
std::optional | Thay thế null returns và sentinels |
std::string_view | Tham số chuỗi zero-cost |
[[nodiscard]] | Ngăn bỏ qua error returns |
if với initializer | Giới hạn phạm vi biến |
Học và dùng có chọn lọc (C++20):
| Tính Năng | Tại Sao |
|---|---|
| Concepts | Lỗi template tốt hơn, ràng buộc rõ ràng hơn |
| Ranges | Pipeline dữ liệu có thể kết hợp, dễ đọc |
std::format | Thay thế printf và stringstream |
| Designated initializers | Xâ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ùngGiả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ì?
- Deep Dive: Quản Lý Bộ Nhớ & Smart Pointers → —
unique_ptr,shared_ptr, và custom allocators chuyên sâu - Deep Dive: STL Containers & Algorithms → — Nắm vững thư viện tiêu chuẩn
Tài Nguyên Bổ Sung
- cppreference.com — Tài liệu tham khảo C++ dứt khoát
- C++ Core Guidelines
- Compiler Explorer (godbolt.org) — Xem output assembly cho bất kỳ code C++ nào
- Effective Modern C++ của Scott Meyers — Sách tốt nhất cho C++11/14
- C++17 in Detail của Bartłomiej Filipek — Hướng dẫn C++17 thực tế
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.