This programming guide is only available in Vietnamese. Switch to Vietnamese to read the full article.
Khi bạn cần viết một thuật toán sắp xếp mảng hay một lớp lưu trữ dữ liệu Cache, thuật toán logic hoạt
động hoàn toàn giống nhau bất kể kiểu dữ liệu là int, float, hay
std::string. Thay vì phải sao chép code ra nhiều bản (gây thừa mã), C++ giới thiệu giải
pháp Lập trình tổng quát (Generic Programming) thông qua
Templates (Khuôn mẫu). Trong bài viết này, chúng ta sẽ viết các templates của riêng
mình và khai thác cấu trúc dữ liệu lưu trữ khoá-giá trị cực mạnh của STL: std::map và
std::unordered_map.
1. Templates hoạt động thế nào? Cơ chế Instantiation & Phình mã (Code Bloat)
Templates thực chất là các bản thiết kế trừu tượng (Blueprints) cho trình biên dịch. Khi bạn định
nghĩa một hàm hoặc lớp mẫu với tham số kiểu dữ liệu T, trình biên dịch chưa sinh ra bất
kỳ đoạn mã máy nào trong file nhị phân.
Quá trình Template Instantiation (Hiện thực hóa khuôn mẫu):
Chỉ khi bạn bắt đầu khởi tạo lớp mẫu (ví dụ: SimpleCache<std::string, int> cache;),
trình biên dịch mới đọc khuôn mẫu, thay thế K = std::string và V = int để tự
động viết ra một lớp thực tế độc lập ngay lúc biên dịch (Compile-time). Quá trình này hoàn tất mà
không phát sinh bất kỳ chi phí (overhead) nào lúc runtime.
Mặt trái của Templates: Hiệu ứng phình mã (Code Bloat)
Vì trình biên dịch sinh ra một khối mã máy mới cho mỗi tổ hợp kiểu dữ liệu riêng biệt mà bạn sử dụng:
-
Nếu bạn gọi
SimpleCachevới 10 kiểu dữ liệu khác nhau, trình biên dịch sẽ tạo ra 10 lớp nhị phân hoàn toàn khác nhau trong file thực thi cuối cùng. - Điều này có thể làm kích thước file thực thi (.exe / .bin) phình to nhanh chóng (Code Bloat), gây lãng phí bộ nhớ đệm lệnh (Instruction Cache - I-Cache) của CPU.
- Cách khắc phục: Thiết kế lớp cha (Base Class) không chứa template để quản lý phần logic phi tham số (ví dụ: kích thước cache, đếm số lượng), sau đó cho lớp con sử dụng template kế thừa lại để tránh lặp mã máy.
2. Phân tích chi tiết STL Maps: Cây đỏ đen vs Bảng băm
Thư viện chuẩn C++ cung cấp hai cấu trúc dữ liệu ánh xạ Khóa - Giá trị kinh điển:
| Đặc tính | std::map (Cây đỏ đen) | std::unordered_map (Bảng băm) |
|---|---|---|
| Cấu trúc dưới RAM | Cây tìm kiếm nhị phân tự cân bằng (Red-Black Tree). Mỗi Node lưu trữ khóa, giá trị, con trỏ trái/phải và bit màu. | Bảng băm (Hash Table) sử dụng cơ chế nối chuỗi (Chaining) để giải quyết xung đột bằng danh sách liên kết. |
| Độ phức tạp | Tìm kiếm, chèn, xóa luôn ổn định ở mức $O(\log n)$. | Trung bình là $O(1)$. Tệ nhất có thể chạm mốc $O(n)$ nếu hàm băm kém gây xung đột băm hàng loạt (Hash Collision). |
| Thứ tự phần tử | Luôn được sắp xếp tăng dần theo Khóa nhờ tính chất của cây nhị phân tìm kiếm. | Không có thứ tự nhất định (ngẫu nhiên tùy thuộc vào mã băm Hash Code). |
| Cache Locality | Kém do các nút cây được cấp phát động rời rạc trên Heap (dễ gây Cache Miss). | Tốt hơn ở mảng Bucket ban đầu, nhưng các chuỗi Node liên kết phía sau vẫn bị phân mảnh. |
3. Code thực hành: templates.cpp
Đoạn mã nguồn mẫu dưới đây minh họa cách tự xây dựng một hệ thống Cache DNS tốc độ cao bằng Class Template và `std::unordered_map`:
#include <iostream>
#include <string>
#include <unordered_map>
// 1. Function Template (Hàm tổng quát hóa)
template <typename T>
T findMax(T a, T b) {
return (a > b) ? a : b;
}
// 2. Class Template (Lớp tổng quát hóa)
template <typename K, typename V>
class SimpleCache {
private:
std::unordered_map<K, V> cacheMap;
public:
void put(K key, V val) {
cacheMap[key] = val;
}
V get(K key) {
if (cacheMap.find(key) != cacheMap.end()) {
return cacheMap[key];
}
return V(); // Trả về giá trị mặc định của kiểu V
}
bool exists(K key) {
return cacheMap.find(key) != cacheMap.end();
}
};
int main() {
// Chạy thử nghiệm Function Template
std::cout << "--- Demo Function Template ---" << std::endl;
std::cout << "So lon hon giua 5 va 10: " << findMax(5, 10) << std::endl;
std::cout << "So lon hon giua 3.14 va 2.71: " << findMax(3.14, 2.71) << std::endl;
std::cout << "Chuoi lon hon: " << findMax(std::string("V8"), std::string("SpiderMonkey")) << std::endl;
// Chạy thử nghiệm Class Template (Hệ thống Cache)
std::cout << "\n--- Demo Class Template (Cache System) ---" << std::endl;
SimpleCache<std::string, std::string> dnsCache;
dnsCache.put("js-tools.org", "104.21.43.120");
dnsCache.put("google.com", "142.250.190.46");
std::string domain = "js-tools.org";
if (dnsCache.exists(domain)) {
std::cout << "IP cua " << domain << " la: " << dnsCache.get(domain) << std::endl;
}
return 0;
}
4. Template Specialization (Chuyên hóa Khuôn mẫu)
Trong một số trường hợp, bạn muốn tạo ra một phiên bản riêng biệt của template cho một kiểu dữ liệu cụ thể hoặc một nhóm kiểu dữ liệu đặc biệt. C++ cung cấp hai loại chuyên hóa: Full Specialization (chuyên hóa đầy đủ) và Partial Specialization (chuyên hóa riêng phần).
4.1 Full Specialization - Chuyên hóa cho một kiểu cụ thể
Khi bạn muốn cung cấp một triển khai hoàn toàn khác cho một kiểu dữ liệu cụ thể, hãy sử dụng full
specialization. Một ví dụ điển hình là std::vector<bool> - đây là một chuyên hóa
đặc biệt để tối ưu hóa không gian lưu trữ bằng bit packing (mỗi phần tử chỉ chiếm 1 bit thay vì 1
byte):
// Template chung (Primary Template)
template <typename T>
class Storage {
public:
void save(T value) {
std::cout << "Lưu trữ generic: " << value << std::endl;
}
};
// Full Specialization cho kiểu bool
template <>
class Storage<bool> {
private:
uint8_t bitField = 0; // Sử dụng 8 bit để lưu tối đa 8 giá trị bool
public:
void save(bool value) {
std::cout << "Lưu trữ bool tối ưu hóa (bit packing): " << (value ? "true" : "false") << std::endl;
}
};
// Full Specialization cho con trỏ
template <typename T>
class Storage<T*> { // Đây là Partial Specialization (chuyên hóa riêng phần)
public:
void save(T* ptr) {
std::cout << "Lưu trữ con trỏ tới: " << typeid(T).name() << std::endl;
}
};
int main() {
Storage<int> intStorage;
intStorage.save(42); // Dùng template chung
Storage<bool> boolStorage;
boolStorage.save(true); // Dùng full specialization
Storage<double*> ptrStorage;
ptrStorage.save(nullptr); // Dùng partial specialization
return 0;
}
4.2 Partial Specialization - Chuyên hóa cho một mẫu kiểu
Partial specialization cho phép bạn chuyên hóa template cho một nhóm kiểu dữ liệu thay vì chỉ một kiểu cụ thể. Điều này hữu ích khi bạn muốn xử lý tất cả con trỏ, mảng, hoặc các container theo cách riêng biệt:
// Template chung cho tất cả kiểu dữ liệu
template <typename T>
class PrintType {
public:
void info() {
std::cout << "Kiểu thường: " << typeid(T).name() << std::endl;
}
};
// Partial Specialization cho mảng T[]
template <typename T, size_t N>
class PrintType<T[N]> {
public:
void info() {
std::cout << "Mảng của " << typeid(T).name() << " với " << N << " phần tử" << std::endl;
}
};
// Partial Specialization cho hàm con trỏ
template <typename R, typename... Args>
class PrintType<R(*)(Args...)> {
public:
void info() {
std::cout << "Con trỏ tới hàm trả về: " << typeid(R).name() << std::endl;
}
};
int main() {
PrintType<int> t1;
t1.info(); // Kiểu thường
PrintType<float[10]> t2;
t2.info(); // Mảng
int (*funcPtr)(int) = nullptr;
PrintType<decltype(funcPtr)> t3;
t3.info(); // Con trỏ hàm
return 0;
}
5. SFINAE (Substitution Failure Is Not An Error)
SFINAE là một kỹ thuật mạnh mẽ cho phép trình biên dịch "im lặng" khi thay thế kiểu dữ liệu vào template gặp lỗi, thay vì phát hiện lỗi. Thay vì đó, trình biên dịch sẽ tìm một template khác phù hợp. Điều này là nền tảng của Template Metaprogramming và Type Traits.
5.1 Ví dụ cơ bản: Phân biệt con trỏ vs giá trị
SFINAE cho phép chúng ta viết các hàm có hành vi khác nhau tùy thuộc vào kiểu dữ liệu:
#include <iostream>
#include <type_traits>
// Phiên bản chung cho tất cả kiểu (mặc định)
template <typename T>
void process(T value, typename std::enable_if<!std::is_pointer<T>::value>::type* = nullptr) {
std::cout << "Xử lý giá trị: " << value << std::endl;
}
// Phiên bản chuyên biệt cho con trỏ (SFINAE loại bỏ phiên bản chung)
template <typename T>
void process(T ptr, typename std::enable_if<std::is_pointer<T>::value>::type* = nullptr) {
std::cout << "Xử lý con trỏ: " << ptr << std::endl;
}
int main() {
int x = 42;
process(x); // Gọi phiên bản đầu tiên
int* ptr = &x;
process(ptr); // Gọi phiên bản thứ hai
return 0;
}
5.2 std::enable_if và Type Traits
std::enable_if là một kỹ thuật từ Standard Library sử dụng SFINAE. Nó chỉ "kích hoạt" một
template nếu một điều kiện compile-time là đúng. Các Type Traits như
std::is_pointer, std::is_integral, std::is_floating_point giúp
bạn kiểm tra tính chất của kiểu dữ liệu:
#include <iostream>
#include <type_traits>
// Chỉ tính tổng với kiểu số học
template <typename T>
typename std::enable_if<std::is_arithmetic<T>::value, T>::type
sum(T a, T b) {
return a + b;
}
// Chỉ hiển thị tên kiểu cho con trỏ
template <typename T>
typename std::enable_if<std::is_pointer<T>::value>::type
printInfo(T ptr) {
std::cout << "Con trỏ kiểu: " << typeid(T).name() << std::endl;
}
int main() {
std::cout << sum(3, 4) << std::endl; // OK: int là kiểu số học
std::cout << sum(3.14, 2.71) << std::endl; // OK: double là kiểu số học
// sum("hello", "world"); // Lỗi compile: string không phải số học
int x = 10;
printInfo(&x); // OK: &x là con trỏ
// printInfo(x); // Lỗi compile: x không phải con trỏ
return 0;
}
6. Variadic Templates (Khuôn mẫu với số lượng tham số thay đổi)
Từ C++11, bạn có thể viết các template chấp nhận số lượng tham số bất kỳ thay đổi. Điều này
giúp bạn viết các hàm và lớp có tính linh hoạt cao, chẳng hạn như hàm printf type-safe
hoặc các utility như std::tuple.
6.1 Parameter Packs và Template Recursion
Một parameter pack (túi tham số) là một tập hợp tham số template hoặc hàm có kích thước không xác định. Để xử lý parameter pack, bạn cần sử dụng template recursion (đệ quy khuôn mẫu):
#include <iostream>
// Base case: không có tham số
void print() {
std::cout << std::endl;
}
// Recursive case: tách tham số đầu tiên, xử lý nó, sau đó gọi lại với phần còn lại
template <typename T, typename... Args>
void print(T first, Args... rest) {
std::cout << first << " ";
print(rest...); // Gọi đệ quy với các tham số còn lại
}
int main() {
print(1, 2.5, "hello", true); // In: 1 2.5 hello 1
// (true được in thành 1 vì std::cout mặc định in bool dưới dạng số)
return 0;
}
6.2 Fold Expressions (C++17)
C++17 giới thiệu fold expressions để xử lý parameter packs mà không cần recursion
phức tạp. Bạn có thể fold (gập) parameter pack bằng các toán tử như +, *,
, (dấu phẩy):
#include <iostream>
// Cộng tất cả tham số (fold expression với +)
template <typename... Args>
int sum(Args... args) {
return (... + args); // Unary left fold: 1 + 2 + 3 + 4 = 10
}
// In tất cả tham số (fold expression với dấu phẩy)
template <typename... Args>
void printAll(Args... args) {
((std::cout << args << " "), ...); // Print từng phần tử
std::cout << std::endl;
}
int main() {
std::cout << sum(1, 2, 3, 4) << std::endl; // In: 10
printAll(10, 20, 30, "hello"); // In: 10 20 30 hello
return 0;
}
7. Concepts (C++20)
C++20 giới thiệu Concepts - một cách hiện đại và rõ ràng hơn để ràng buộc template parameters. Thay vì dùng SFINAE phức tạp, bạn có thể viết các yêu cầu ngôn ngữ tự nhiên hơn cho những gì một kiểu dữ liệu phải làm.
7.1 Định nghĩa và sử dụng Concepts
Một concept là một tập hợp các yêu cầu (requirements) mà một kiểu dữ liệu phải thỏa mãn:
#include <iostream>
#include <concepts>
// Định nghĩa một concept: kiểu T phải có thể cộng với chính nó
template <typename T>
concept Addable = requires(T a, T b) {
{ a + b } -> std::convertible_to<T>;
};
// Hàm chỉ hoạt động với kiểu thỏa mãn concept Addable
template <Addable T>
T add(T a, T b) {
return a + b;
}
// Concept cho các kiểu có iterator
template <typename T>
concept Iterable = requires(T t) {
t.begin();
t.end();
};
template <Iterable T>
void printContainer(T container) {
for (auto element : container) {
std::cout << element << " ";
}
std::cout << std::endl;
}
int main() {
std::cout << add(5, 3) << std::endl; // OK: 8
std::cout << add(2.5, 3.5) << std::endl; // OK: 6.0
std::vector<int> vec = {1, 2, 3};
printContainer(vec); // OK: 1 2 3
// add("hello", "world"); // Lỗi compile: string + string không trả về string
// printContainer(42); // Lỗi compile: int không có begin()/end()
return 0;
}
8. Template Metaprogramming (Lập trình Khuôn mẫu)
Template Metaprogramming (TMP) là kỹ thuật sử dụng các template để thực hiện tính toán tại thời điểm biên dịch (compile-time). Thay vì tính toán khi chương trình chạy, các kết quả được tính và mã hóa vào chương trình nhị phân sẵn.
8.1 Tính Fibonacci tại Compile-time
Một ví dụ điển hình là tính Fibonacci recursively bằng template specialization:
#include <iostream>
// Template recursion để tính Fibonacci tại compile-time
template <int N>
struct Fibonacci {
static constexpr int value = Fibonacci<N-1>::value + Fibonacci<N-2>::value;
};
// Base cases
template <>
struct Fibonacci<0> {
static constexpr int value = 0;
};
template <>
struct Fibonacci<1> {
static constexpr int value = 1;
};
int main() {
// Tất cả những tính toán này xảy ra tại compile-time, không có chi phí runtime
std::cout << "Fibonacci(5) = " << Fibonacci<5>::value << std::endl; // 5
std::cout << "Fibonacci(10) = " << Fibonacci<10>::value << std::endl; // 55
std::cout << "Fibonacci(15) = " << Fibonacci<15>::value << std::endl; // 610
return 0;
}
9. So sánh: Function Templates vs Class Templates vs Lambdas
Mỗi cách tiếp cận có những ưu nhược điểm riêng:
| Đặc tính | Function Templates | Class Templates | Lambdas (C++11) |
|---|---|---|---|
| Phạm vi sử dụng | Chức năng độc lập (hàm toàn cục, static) | Lưu trữ trạng thái, kế thừa, multiple templates | Callback, xử lý event, higher-order functions |
| Lưu trữ trạng thái (State) | Không (chỉ biến static hoặc tham số) | Có (member variables) | Có (capture list) |
| Độ phức tạp compile | Thấp, trình biên dịch dễ suy diễn kiểu | Cao, phải chỉ định tất cả template parameters | Rất thấp, auto inference luôn hoạt động |
| Hỗ trợ SFINAE / Concepts | Có, dễ dàng | Có, nhưng phức tạp hơn | Không (lambdas không phải template) |
| Kích thước code | Nhỏ hơn (chia sẻ code logic) | Lớn (tiềm ẩn code bloat) | Thường nhỏ nhất (inline) |
| Ví dụ sử dụng |
std::sort, std::find, custom findMax()
|
std::vector, std::map, SimpleCache
|
std::for_each, click handlers, thread lambdas |
10. Diagram: Quá trình Template Instantiation
Dưới đây là sơ đồ minh họa cách trình biên dịch xử lý templates:
Mã nguồn:
┌─────────────────────────────────────┐
│ template <typename T> │
│ void print(T value); │
│ │
│ print(42); // T=int│
│ print(3.14); // T=double│
│ print("hello"); // T=const char*│
└─────────────────────────────────────┘
│
│ Compile-time (Trình biên dịch thực hiện)
▼
┌─────────────────────────────────────┐
│ Template Instantiation: │
├─────────────────────────────────────┤
│ 1. print<int>(int) │
│ 2. print<double>(double) │
│ 3. print<const char*>(const char*)│
└─────────────────────────────────────┘
│
│ Object Code (.o / .obj)
▼
┌─────────────────────────────────────┐
│ Linker kết hợp các hàm │
│ print<int>, print<double>, │
│ print<const char*> thành │
│ một file thực thi duy nhất │
└─────────────────────────────────────┘
│
│ Runtime: không có chi phí template
▼
┌─────────────────────────────────────┐
│ Khi chương trình chạy: │
│ - Gọi print<int>(42) │
│ - Gọi print<double>(3.14) │
│ - Gọi print<const char*>("hello") │
│ Tất cả đều có tốc độ cực nhanh! │
└─────────────────────────────────────┘
Câu hỏi ôn tập kiến thức
Chi phí hiệu năng tại thời điểm runtime (khi chương trình đang chạy) của một hàm sử dụng C++ Template so với một hàm thông thường được viết thủ công là bao nhiêu?
Tải mã nguồn mẫu bài học
Bạn có thể tải file mã nguồn C++ mẫu hoàn chỉnh của bài học này để tiến hành thực hành trực tiếp trên máy tính cá nhân.
Tải templates.cpp
Comments
Bình luận