Chuyển đến nội dung

Mẫu hàm & Khấu trừ kiểu

Tìm hiểu cách viết các hàm hoạt động với bất kỳ loại nào. Hiểu cách suy luận đối số mẫu, tham số không phải loại và lý do mẫu nằm trong tiêu đề.

Tại sao lại là Mẫu hàm?

Không có mẫu, việc viết một hàm hoạt động cho nhiều loại có nghĩa là logic trùng lặp. Bạn sẽ viếtint max(int, int),double max(double, double),std::string max(std::string, std::string)- logic giống hệt nhau được lặp lại cho mọi loại. Mẫu hàm cho phép bạn viết thuật toán một lần và để trình biên dịch tự động tạo các phiên bản dành riêng cho từng loại.

Mẫu hàm không phải là một hàm — nó là một bản thiết kế mà từ đó trình biên dịch khởi tạo các hàm thực tế khi bạn sử dụng chúng với các kiểu cụ thể. Điều này xảy ra hoàn toàn vào thời gian biên dịch, tạo ra mã giống hệt với những gì bạn viết bằng tay — không tốn chi phí.

Cú pháp mẫu

Mẫu hàm được giới thiệu với từ khóa template, theo sau là danh sách tham số trong dấu ngoặc nhọn. Bạn có thể sử dụng typename hoặc class thay thế cho nhau cho các tham số loại — chúng có nghĩa giống hệt nhau. Quy ước hiện đại thíchtypename.

function_template.cpp
#include <iostream>
#include <string>

// 'typename' and 'class' are interchangeable here
template <typename T>
T maximum(T a, T b) {
    return (a > b) ? a : b;
}

// Equivalent — 'class' means the same thing
template <class T>
T minimum(T a, T b) {
    return (a < b) ? a : b;
}

int main() {
    std::cout << maximum(3, 7)          << '\n'; // deduces T = int
    std::cout << maximum(3.14, 2.71)    << '\n'; // deduces T = double
    std::cout << maximum<std::string>("hello", "world") << '\n';
    // explicit instantiation needed — "hello" is const char*, not std::string
}

Quy tắc khấu trừ đối số mẫu

Trình biên dịch suy ra các đối số mẫu bằng cách khớp loại đối số hàm với mẫu tham số. Các quy tắc chính là:

1. Ưu tiên đối sánh chính xác —maximum(3, 7)suy raT = intvì cả hai đối số đều làint.
2. Không có chuyển đổi tiềm ẩn trong quá trình khấu trừ —maximum(3, 7.0)là lỗi: trình biên dịch nhìn thấyT = inttừ đối số đầu tiên vàT = doubletừ đối số thứ hai và từ chối chọn một đối số.
3. Phân rã từ mảng sang con trỏ — khiTxuất hiện dưới dạng tham số giá trị, mảng sẽ phân rã thành con trỏ. Sử dụngT(&)[N]để nắm bắt loại và kích thước mảng.
4. Hằng số/dễ bay hơi cấp cao nhất bị loại bỏ khỏi các tham số giá trị nhưng được giữ nguyên cho các tham số tham chiếu/con trỏ.
5. Thu gọn tham chiếu — nếuTđược suy ra dưới dạng loại tham chiếu (trong chuyển tiếp tham chiếu), quy tắc thu gọn tham chiếu sẽ áp dụng:T& &trở thànhT&,T& &&trở thànhT&,T&& &trở thànhT&,T&& &&trở thànhT&&.

Khi suy luận không thành công hoặc không rõ ràng, bạn phải cung cấp đối số mẫu rõ ràng:maximum(3, 7.0).

Tham số mẫu không phải loại

Tham số mẫu không giới hạn loại. Tham số mẫu không phải loại (NTTP) chấp nhận các giá trị thời gian biên dịch chẳng hạn như số nguyên, con trỏ hoặc loại dấu phẩy động và lớp (kể từ C++20). Chúng rất mạnh để nhúng các hằng số thời gian biên dịch vào hệ thống kiểu.

non_type_params.cpp
#include <iostream>
#include <array>
#include <cstddef>

// Non-type parameter: N is a compile-time size_t value
template <typename T, std::size_t N>
T sum(const std::array<T, N>& arr) {
    T total{};
    for (const auto& elem : arr) {
        total += elem;
    }
    return total;
}

// C++20: auto non-type parameter accepts any structural type
template <auto Value>
constexpr auto doubled = Value * 2;

int main() {
    std::array<int, 4> nums = {1, 2, 3, 4};
    std::cout << sum(nums) << '\n';     // 10, deduces T=int, N=4

    std::cout << doubled<21> << '\n';   // 42
    std::cout << doubled<3.14> << '\n'; // 6.28 (C++20)
}
Ghi chú

Mỗi sự kết hợp duy nhất của các đối số mẫu sẽ khiến trình biên dịch tạo ra một bản sao riêng biệt — một bản sao riêng biệt của hàm trong tệp nhị phân của bạn. Gọimaximum,maximumvàmaximumtạo ra ba chức năng. Điều này thường ổn, nhưng trong các cơ sở mã lớn, các mẫu có thể góp phần gây ra sự phình to mã (các tệp nhị phân lớn hơn, thời gian biên dịch dài hơn). Các chiến lược giảm thiểu bao gồm: sử dụng các khai báo mẫu bên ngoài để kiểm soát nơi diễn ra quá trình khởi tạo, phân tích mã độc lập với loại ra khỏi các mẫu và sử dụng xóa kiểu khi đa hình thời gian chạy được chấp nhận.

Lỗi thường gặp

Vì trình biên dịch cần xem định nghĩa mẫu đầy đủ để khởi tạo nó nên các mẫu không thể được chia thành các tệp.h/.cpptheo cách mà các hàm thông thường có thể làm. Nếu bạn đặt định nghĩa mẫu vào tệp.cppvà cố gắng sử dụng nó từ một đơn vị dịch thuật khác, bạn sẽ gặp lỗi trình liên kết (tham chiếu không xác định). Giải pháp tiêu chuẩn là đặt toàn bộ định nghĩa mẫu vào tệp tiêu đề. Nếu thời gian biên dịch trở thành vấn đề, bạn có thể sử dụng khởi tạo rõ ràng: đặt định nghĩa vào tệp.cppvà thêmtemplate int maximum(int, int);để khởi tạo rõ ràng các chuyên môn bạn cần.

Khởi tạo rõ ràng

Bạn có thể kiểm soát rõ ràng vị trí và thời điểm mẫu được khởi tạo. Điều này có thể giảm thời gian biên dịch trong các dự án lớn bằng cách ngăn chặn việc khởi tạo dư thừa trên các đơn vị dịch thuật.

explicit_instantiation.h
// ---- maximum.h ----
template <typename T>
T maximum(T a, T b) {
    return (a > b) ? a : b;
}

// Suppress implicit instantiation in every TU that includes this header
extern template int maximum<int>(int, int);
extern template double maximum<double>(double, double);

// ---- maximum.cpp ----
#include "maximum.h"

// Explicit instantiation: generate code here, once
template int maximum<int>(int, int);
template double maximum<double>(double, double);
Điểm chính
  • Mẫu hàm là bản thiết kế - trình biên dịch tạo ra các hàm cụ thể cho từng bộ đối số mẫu
  • typenamevàclasscó thể hoán đổi cho nhau trong danh sách tham số mẫu
  • Việc khấu trừ đối số mẫu không thực hiện chuyển đổi ngầm định - các khoản khấu trừ không rõ ràng là lỗi
  • Các tham số mẫu không phải kiểu nhúng các giá trị thời gian biên dịch vào hệ thống kiểu
  • Mẫu phải được xác định trong tiêu đề (hoặc sử dụng cách khởi tạo rõ ràng) vì trình biên dịch cần định nghĩa đầy đủ để khởi tạo

Câu đố — Kiểm tra kiến thức

(15 XP)

1. Điều gì xảy ra khi bạn gọi`maximum(3, 7.0)`bằng mẫu`template<typename T> T maximum(T a, T b)`?

2. Tại sao các định nghĩa mẫu thường phải được đặt trong các tệp tiêu đề?

3. Mục đích của`extern template int maximum<int>(int, int);`là gì?