Template trong C++: Kỹ thuật lập trình nâng cao

Template trong C++ được coi là một hình thức nghệ thuật đặc biệt của ngôn ngữ này, thậm chí có thể được xem như một ngôn ngữ lập trình riêng biệt. Triết lý của template là xử lý mọi vấn đề có thể giải quyết tại thời điểm biên dịch, chỉ để lại những dịch vụ động cốt lõi cho thời gian chạy, từ đó tối ưu hóa hiệu suất đáng kể. Vì vậy, nhiều người xem template như một trong những "phép màu đen" của C++.

Trước hết, hai từ khóa typenameclass trong template C++ có ý nghĩa giống hệt nhau. Lý do cả hai đều được sử dụng và có ý nghĩa giống nhau là do Stan Lippman đã giải thích trong blog của mình rằng, ban đầu Stroustrup sử dụng class để khai báo các tham số trong template để tránh thêm từ khóa không cần thiết; sau này, ủy ban chuẩn hóa cho rằng việc sử dụng đồng thời có thể gây nhầm lẫn về khái niệm nên đã thêm từ khóa typename.

Từ khóa typename được sử dụng để báo cho trình biên dịch C++ biết rằng chuỗi ký tự sau typename là một tên kiểu, không phải là hàm thành viên hoặc biến thành viên. Nếu không có typename ở phía trước, trình biên dịch không thể biết T::LengthType là một kiểu hay một tên thành viên (thành viên dữ liệu tĩnh hoặc hàm tĩnh), do đó mã không thể biên dịch được.

Đối với các tên phụ thuộc vào tham số template trong định nghĩa template, chỉ khi tham số được thực thi chứa tên kiểu này, hoặc tên này được sử dụng từ khóa typename để sửa đổi, trình biên dịch mới coi đó là một kiểu. Ngoài hai trường hợp trên, nó sẽ không bao giờ được coi là kiểu.

Vì vậy, nếu bạn muốn trực tiếp nói với trình biên dịch rằng T::const_iterator là kiểu chứ không phải biến, chỉ cần sử dụng typename để sửa đổi:

template<kieu T>
void ham(const T& thamSo, typename T::const_iterator it);

Trong ví dụ trên, tham số thứ hai của hàm ham là giá trị của kiểu T::const_iterator (lặp của T) phụ thuộc vào T. Các tên phụ thuộc vào tham số template trong template được gọi là tên phụ thuộc (dependent name), khi một tên phụ thuộc được lồng trong một lớp, nó được gọi là tên phụ thuộc lồng (nested dependent name). Thực tế, T::const_iterator vẫn là tên kiểu phụ thuộc lồng (nested dependent type name).

Khi sử dụng template, chúng ta có thể chỉ định rõ ràng các tham số template, hoặc để trình biên dịch tự động suy luận:

template<kieu T1, kieu T2, kieu T3>
T1 tong(T2 t2, T3 t3)
{
    return t2 + t3;
}
 
<long long> tong(10,200); // hoặc <long long, int, int>();

Tuy nhiên, cần lưu ý rằng khi tự động suy luận, cần đảm bảo các tham số đầu vào có thể thỏa mãn một số thao tác toán tử hoặc chuyển đổi ngầm an toàn.

Đối với template, chỉ khi sử dụng mới được thực thi, nhưng mặc dù trước khi thực thi, trình biên dịch chưa tạo ra mã cụ thể, chúng ta vẫn có thể thực hiện các thao tác gắn con trỏ hàm.

template <kieu T> int sosanh(const T & t1, const T & t2) { }
 
int (*sosanh_int)(const int &,const int &) = sosanh;  //cần viết rõ ràng tham số template ở đây

Tương tự, chúng ta cũng có thể truyền hàm template làm hàm gọi lại, nhưng lúc này có thể gây ra tính đa nghĩa, vì vậy cần lưu ý viết rõ ràng các tham số template.

Chuyên biệt hóa Template

Chuyên biệt hóa hàm template:

Nếu một số kiểu đặc biệt không thể sử dụng template để lập trình tổng quát, thì cần sử dụng chuyên biệt hóa template để thực hiện riêng, ví dụ như sau:

template<kieu T>
bool BangNhau(T a, T b){
    return a==b;
}
template<>
bool BangNhau(char *a, char *b){
    if(strcmp(a,b)==0)return true;
    return false;      
}

Ví dụ trên định nghĩa một hàm template BangNhau, vì việc so sánh đối tượng kiểu char* không thể sử dụng trực tiếp ==, vì vậy cần thực hiện chuyên biệt hóa dữ liệu kiểu cụ thể.

template không có tham số ở trên là chuyên biệt hóa hoàn toàn, nếu không có template thì là nạp chồng hàm thông thường, cần lưu ý rằng trong C++, đối với hàm nạp chồng, hàm thông thường có độ ưu tiên cao nhất, tiếp theo là hàm template.

Chuyên biệt hóa lớp template:

Chuyên biệt hóa hoàn toàn lớp template tương tự như hàm template, không cần giải thích thêm, nhưng trong lớp template còn có một loại chuyên biệt hóa đặc biệt---chuyên biệt hóa một phần, tức là giữ lại các tham số kiểu cố định trong template (lưu ý, hàm template không hỗ trợ chuyên biệt hóa một phần)

template<kieu T1, kieu T2>    //lớp template thông thường
class test{
...
};
template<kieu T1, kieu T2>
class test<T1*,T2*>{    //chuyên biệt hóa một phần (đối với con trỏ thông thường)
...
};
template<kieu T1, kieu T2>
class test<const T1*,T2*>{    //chuyên biệt hóa một phần (đối với con trỏ const)
...
};
//ví dụ định nghĩa lớp vector trong thư viện chuẩn C++
template <class T, class PhanBo>
class vector { // … // };
template <class PhanBo>    //trong ví dụ chuyên biệt hóa một phần này, một tham số được ràng buộc đến kiểu bool, trong khi tham số còn lại vẫn chưa được ràng buộc và cần do người dùng chỉ định.
class vector<bool, PhanBo> { //…//};

Template tham số biến

Khi số lượng tham số không thể xác định, có thể sử dụng template tham số biến, ví dụ sử dụng như sau:

template<kieu... Ts> class ThuKy;    //xác định số lượng tham số 0
template<kieu YeuCau, kieu... Args> class ThuKy;    //xác định số lượng tham số 1
template<kieu... Args> void in(const std::string &str, Args... args);    //template hàm tham số biến

Đối với việc giải nén tham số biến, chúng ta có thể sử dụng sizeof... để tính toán số lượng tham số

template<kieu... Ts>
void phieuThu(Ts... args) {
    std::cout << sizeof...(args) << std::endl;
}

Thứ hai, đối với việc giải nén tham số, cho đến nay vẫn chưa có một phương pháp đơn giản nào có thể xử lý gói tham số, nhưng có hai phương pháp xử lý kinh điển:

1. Hàm template đệ quy

Đệ quy là một phương pháp dễ nghĩ ra nhất, cũng là phương pháp kinh điển nhất. Phương pháp này liên tục truyền tham số template vào hàm, đạt được mục đích duyệt qua tất cả các tham số template:

template<kieu T0>
void in1(T0 value) {
    std::cout << value << std::endl;
}
template<kieu T, kieu... Ts>
void in1(T value, Ts... args) {
    std::cout << value << std::endl;
    in1(args...);
}

2. Mở rộng template tham số biến

Trong C++17 đã thêm hỗ trợ mở rộng template tham số biến, do đó bạn có thể hoàn thành việc viết printf trong một hàm:

template<kieu T0, kieu... T>
void in2(T0 t0, T... t) {
    std::cout << t0 << std::endl;
    if constexpr (sizeof...(t) > 0) in2(t...);
}<br></br>

Suy luận tham số template không kiểu

Trước đây chúng ta chủ yếu đề cập đến một hình thức của tham số template: tham số kiểu template---thay thế các tham số khác nhau cho kiểu

Còn một loại template khác có thể cho các hằng số trở thành tham số template, tức là tham số template không kiểu, như sau:

template <kieu T, int KichThuoc>
class boDem {
public:
    T& phanBo();
    void giaiPhong(T& item);
private:
    T data[KichThuoc];
}
boDem<int, 100> buf; // 100 làm tham số template

Mã trên ở đây tham số template int KichThuoc chỉ rõ kiểu, có thể sử dụng trực tiếp như kiểu constexpr int, và khi sử dụng template, truyền một giá trị cụ thể làm tham số template.

Ở đây chúng ta sử dụng 100 làm tham số template truyền vào, trong C++17 cho phép sử dụng từ khóa auto để trình biên dịch hỗ trợ hoàn thành suy luận kiểu:

template <auto value> void hamThu() {
    std::cout << value << std::endl;
    return;
}
int main() {
    hamThu<10>();  // value được suy luận là kiểu int
}

Template bên ngoài

Trong C++ truyền thống, template chỉ được trình biên dịch thực thi khi sử dụng. Nói cách khác, miễn là trong mã được biên dịch trong mỗi đơn vị biên dịch (tệp) gặp định nghĩa template đầy đủ, nó sẽ được thực thi. Điều này gây ra việc thực thi lại, làm tăng thời gian biên dịch. Và chúng ta không có cách nào thông báo cho trình biên dịch không kích hoạt việc thực thi template.

Vì vậy, C++11 đã giới thiệu template bên ngoài, mở rộng cú pháp ban đầu buộc trình biên dịch thực thi template tại vị trí cụ thể, cho phép chúng ta thông báo rõ ràng cho trình biên dịch khi nào thực thi template:

template class std::vector<bool>;          // ép buộc thực thi
extern template class std::vector<double>; // không thực thi template trong tệp biên dịch hiện tại

Bí danh kiểu template

Đối với template, không thể sử dụng trực tiếp typedef để đặt bí danh cho template, nhưng C++11 đã giới thiệu using, có thể đặt bí danh cho template, như sau

template<kieu T, kieu U>
class LoiThu {
public:
    T am;
    U phuong;
};

// không hợp lệ
template<kieu T>
typedef LoiThu<std::vector<T>, std::string> AmThan;
//hợp lệ
typedef int (*xuLy)(void *);
using newXuLy = int(*)(void *);
template<kieu T>
using AmThanThat = LoiThu<std::vector<T>, std::string>;

Tham khảo bài viết: https://changkun.de/modern-cpp/zh-cn/02-usability/#%E7%B1%BB%E5%9E%8B%E5%88%AB%E5%90%8D%E6%A8%A1%E6%9D%BF

https://blog.csdn.net/lyn631579741/article/details/110730145

Thẻ: template-cpp lap-trinh-mau c++-nang-cao kieu-du-lieu thuật-toán

Đăng vào ngày 21 tháng 6 lúc 17:51