Mẫu siêu lập trình mẫu
Nắm vững các mẫu mẫu nâng cao: CRTP dành cho tính đa hình tĩnh, thiết kế dựa trên chính sách để có thành phần linh hoạt, xóa kiểu cho các mẫu chung trong thời gian chạy và hướng dẫn về thời điểm sử dụng mẫu so với công văn ảo.
Ngoài các mẫu cơ bản
Mẫu cho phép các mẫu thiết kế không có mẫu tương đương trong các ngôn ngữ khác. Các mẫu này khai thác tính chất thời gian biên dịch của các mẫu để đạt được tính đa hình tĩnh (không có chi phí chức năng ảo), hành vi có thể tổng hợp (chính sách) và xóa an toàn kiểu (ẩn các loại cụ thể đằng sau một giao diện thống nhất).
Đây không phải là những mẫu dành cho người mới bắt đầu - chúng là công cụ dành cho các tác giả thư viện và nhà thiết kế khung. Hiểu chúng là điều cần thiết để đọc nội bộ thư viện tiêu chuẩn, Boost và các khung C++ hiện đại.
CRTP: Mẫu mẫu định kỳ kỳ lạ
Trong CRTP, một lớp dẫn xuất từ một mẫu được khởi tạo với chính nó làm đối số:class Derived : public Base. Lớp cơ sở có thể gọi các phương thức trên lớp dẫn xuất tại thời điểm biên dịch — không cần chi phí điều phối ảo. Điều này được sử dụng để chèn hành vi, triển khai đa hình tĩnh và tạo chức năng mixin.
#include <iostream>
// CRTP base: provides a counter for each derived class
template <typename Derived>
class Counter {
static inline int count_ = 0;
protected:
Counter() { ++count_; }
~Counter() { --count_; }
Counter(const Counter&) { ++count_; }
public:
static int alive() { return count_; }
};
// Each derived class gets its OWN counter
class Dog : public Counter<Dog> {
public:
std::string name;
Dog(std::string n) : name(std::move(n)) {}
};
class Cat : public Counter<Cat> {
public:
std::string name;
Cat(std::string n) : name(std::move(n)) {}
};
// CRTP for static polymorphism: no virtual dispatch
template <typename Derived>
class Shape {
public:
double area() const {
// Call derived class method — resolved at compile time
return static_cast<const Derived*>(this)->area_impl();
}
};
class Circle : public Shape<Circle> {
double radius_;
public:
Circle(double r) : radius_(r) {}
double area_impl() const { return 3.14159265 * radius_ * radius_; }
};
class Square : public Shape<Square> {
double side_;
public:
Square(double s) : side_(s) {}
double area_impl() const { return side_ * side_; }
};
int main() {
Dog d1("Rex"), d2("Buddy");
Cat c1("Whiskers");
std::cout << "Dogs alive: " << Dog::alive() << '\n'; // 2
std::cout << "Cats alive: " << Cat::alive() << '\n'; // 1
Circle circle(5.0);
Square square(4.0);
std::cout << "Circle area: " << circle.area() << '\n'; // 78.54
std::cout << "Square area: " << square.area() << '\n'; // 16.0
}Thiết kế dựa trên chính sách
Thiết kế dựa trên chính sách phân tách một lớp thành các thành phần hành vi (chính sách) có thể hoán đổi cho nhau được truyền dưới dạng tham số mẫu. Mỗi chính sách thực hiện một khía cạnh của hành vi. Người dùng soạn sự kết hợp chính xác mà họ cần. Điều này đã được phổ biến rộng rãi nhờ "Thiết kế C++ hiện đại" của Andrei Alexandrescu.
#include <iostream>
#include <mutex>
#include <stdexcept>
// Threading policies
struct SingleThreaded {
struct Lock { Lock(SingleThreaded&) {} }; // no-op
};
struct MultiThreaded {
std::mutex mtx;
struct Lock {
std::lock_guard<std::mutex> guard;
Lock(MultiThreaded& host) : guard(host.mtx) {}
};
};
// Bounds checking policies
struct NoBoundsCheck {
static void check(int /*index*/, int /*size*/) {} // no-op
};
struct StrictBoundsCheck {
static void check(int index, int size) {
if (index < 0 || index >= size)
throw std::out_of_range("Index out of bounds");
}
};
// Policy-based container: users choose their combination
template <typename T,
typename ThreadingPolicy = SingleThreaded,
typename BoundsPolicy = NoBoundsCheck>
class SmartArray : private ThreadingPolicy {
T* data_;
int size_;
public:
SmartArray(int size) : data_(new T[size]{}), size_(size) {}
~SmartArray() { delete[] data_; }
T& operator[](int index) {
typename ThreadingPolicy::Lock lock(*this);
BoundsPolicy::check(index, size_);
return data_[index];
}
int size() const { return size_; }
};
int main() {
// Fast, no overhead: no locking, no bounds checks
SmartArray<int> fast(10);
fast[0] = 42;
// Safe: bounds-checked, single-threaded
SmartArray<int, SingleThreaded, StrictBoundsCheck> safe(10);
safe[0] = 42;
try {
safe[99] = 0; // throws!
} catch (const std::out_of_range& e) {
std::cout << e.what() << '\n';
}
// Thread-safe with bounds checking
SmartArray<int, MultiThreaded, StrictBoundsCheck> robust(10);
robust[5] = 100;
}Loại Xóa
Xóa kiểu ẩn kiểu cụ thể đằng sau một giao diện thống nhất, kết hợp tính linh hoạt của các mẫu với tính đa hình khi chạy của các hàm ảo. Ví dụ cổ điển làstd::function, có thể chứa bất kỳ lệnh gọi nào. Mẫu này sử dụng ba thành phần: một khái niệm (giao diện trừu tượng), một model (trình bao bọc mẫu triển khai giao diện cho bất kỳ loại nào) và một lớp xử lý bên ngoài.
#include <iostream>
#include <memory>
#include <string>
// Type-erased "Printable" — can hold any type with a print() method
class Printable {
// Concept: abstract interface
struct Concept {
virtual ~Concept() = default;
virtual void print(std::ostream& os) const = 0;
virtual std::unique_ptr<Concept> clone() const = 0;
};
// Model: wraps any T that supports operator<<
template <typename T>
struct Model : Concept {
T value;
Model(T v) : value(std::move(v)) {}
void print(std::ostream& os) const override { os << value; }
std::unique_ptr<Concept> clone() const override {
return std::make_unique<Model>(value);
}
};
std::unique_ptr<Concept> ptr_;
public:
// Constructor: accepts any printable type
template <typename T>
Printable(T value) : ptr_(std::make_unique<Model<T>>(std::move(value))) {}
// Copy support
Printable(const Printable& other) : ptr_(other.ptr_->clone()) {}
Printable& operator=(const Printable& other) {
ptr_ = other.ptr_->clone();
return *this;
}
Printable(Printable&&) = default;
Printable& operator=(Printable&&) = default;
friend std::ostream& operator<<(std::ostream& os, const Printable& p) {
p.ptr_->print(os);
return os;
}
};
int main() {
// Heterogeneous collection — no common base class needed!
std::vector<Printable> items;
items.emplace_back(42);
items.emplace_back(3.14);
items.emplace_back(std::string("hello"));
items.emplace_back('X');
for (const auto& item : items) {
std::cout << item << '\n';
}
// Output: 42, 3.14, hello, X
}Mẫu biểu thức (Tổng quan)
Mẫu biểu thức là một kỹ thuật trong đó các biểu thức số học trên các đối tượng xây dựng một cây thời gian biên dịch các phép toán thay vì tính toán các kết quả trung gian. Kết quả cuối cùng được tính toán trong một lần duy nhất, loại bỏ các đối tượng tạm thời.
Ví dụ: với lớpVectorđơn giản,a + b + ctạo hai vectơ tạm thời. Với các mẫu biểu thức,a + b + cxây dựng một đối tượng biểu thức nhẹAddđể tính toán các phần tử theo yêu cầu — không cần thời gian tạm thời.
Kỹ thuật này được sử dụng bởi các thư viện đại số tuyến tính hiệu suất cao như Eigen và Blaze. Cây biểu thức được đánh giá một cách lười biếng và trình biên dịch thường có thể vector hóa vòng lặp một lượt kết quả.
Các mẫu biểu thức rất phức tạp để triển khai chính xác và chủ yếu là mối quan tâm của tác giả thư viện. Nếu bạn cần toán ma trận/vectơ, hãy sử dụng thư viện như Eigen thay vì tự mình triển khai các mẫu biểu thức.
Khi chọn giữa mẫu (đa hình tĩnh) và hàm ảo (đa hình động):
Sử dụng mẫu khi:
- Tập hợp các kiểu được biết tại thời điểm biên dịch
- Bạn cần sự trừu tượng hóa không có chi phí (không có vtable, không có hướng dẫn)
- Bạn muốn trình biên dịch nội tuyến và tối ưu hóa trên các ranh giới loại
- Bạn cần làm việc với ngữ nghĩa giá trị (không phân bổ heap)
- Hiệu suất là rất quan trọng (vòng chặt chẽ, mã số)
Sử dụng chức năng ảo khi:
- Tập hợp các loại được mở và xác định trong thời gian chạy (plugin, đầu vào của người dùng)
- Bạn cần các thùng chứa không đồng nhất (ví dụ:vector)
- Kích thước nhị phân quan trọng hơn tốc độ (mẫu tạo ra nhiều mã hơn)
- Bạn muốn ABI ổn định trên khắp ranh giới thư viện dùng chung
- Bạn cần thêm các loại mới mà không cần biên dịch lại mã hiện có
Sử dụng kiểu xóa khi:
- Bạn muốn giao diện đơn giản của điều phối ảo nhưng không muốn buộc người dùng phải kế thừa từ lớp cơ sở của mình
- Bạn cần ngữ nghĩa giá trị với tính đa hình thời gian chạy (std::function,std::any)
Lập trình siêu dữ liệu mẫu rất mạnh mẽ nhưng có chi phí thực sự:
1. Bùng nổ thời gian biên dịch — việc khởi tạo mẫu lồng nhau sâu có thể khiến thời gian biên dịch không được chấp nhận. TMP đệ quy đặc biệt tệ - thích các biểu thức gấp vàif constexpr.
2. Thông báo lỗi — không có khái niệm, lỗi mẫu sẽ tạo ra các trang có văn bản khó hiểu. Luôn hạn chế các mẫu.
3. Gỡ lỗi — việc duyệt qua mã nặng mẫu trong trình gỡ lỗi là điều khó khăn. Trình bao bọc được xóa kiểu thêm hướng dẫn ẩn loại thực tế.
4. Dễ đọc — CRTP, thiết kế dựa trên chính sách và các mẫu biểu thức rất khó hiểu đối với các nhà phát triển cấp dưới. Ghi lại tên mẫu và ý định.
5. Kỹ thuật quá mức — không phải mọi vấn đề đều cần giải pháp mẫu. Một hàm ảo đơn giản hoặc thậm chí một câu lệnh switch có thể rõ ràng và đầy đủ hơn.
- CRTP cho phép đa hình tĩnh và bộ đếm/hỗn hợp mỗi lớp bằng cách lấy từ
Base - Thiết kế dựa trên chính sách phân tách hành vi thành các tham số mẫu — người dùng soạn kết hợp chính xác mà họ cần
- Xóa kiểu kết hợp tính linh hoạt của mẫu với đa hình thời gian chạy (ví dụ:
std::function,std::any) - Mẫu biểu thức loại bỏ các khoảng thời gian tạm thời trong các hoạt động theo chuỗi — được sử dụng bởi Eigen, Blaze và các thư viện tương tự
- Chọn mẫu cho các loại đã biết và không tốn chi phí; công văn ảo cho các bộ kiểu mở và tính linh hoạt của thời gian chạy
- Tránh sử dụng kỹ thuật quá mức — sử dụng cơ chế đơn giản nhất đáp ứng yêu cầu của bạn
Câu đố — Kiểm tra kiến thức
(20 XP)1. Trong CRTP,`class Dog : public Counter<Dog>`đạt được điều gì?
2. Loại tẩy xóa giải quyết được vấn đề gì?
3. Khi nào bạn nên ưu tiên các hàm ảo hơn các mẫu?