Chuyển đến nội dung

Chuyên môn hóa mẫu & Gửi thẻ

Nắm vững chuyên môn đầy đủ và một phần, tìm hiểu khi nào nên ưu tiên quá tải và khám phá việc gửi thẻ và if constexpr làm lựa chọn thay thế cho phân nhánh thời gian biên dịch.

Tại sao chuyên?

Mẫu chính cung cấp cách triển khai chung phù hợp với hầu hết các loại. Nhưng đôi khi một loại cụ thể cần được điều trị khác nhau về cơ bản. Ví dụ:std::vectorlà một chuyên môn đóng gói các bool thành bit để đạt hiệu quả về không gian (mặc dù nó được nhiều người coi là một lỗi trong thư viện chuẩn).

Chuyên môn hóa mẫu cho phép bạn cung cấp định nghĩa thay thế cho các đối số mẫu cụ thể. Có hai loại:

1. Chuyên môn đầy đủ (rõ ràng) — bạn sửa tất cả tham số mẫu cho các loại cụ thể.
2. Chuyên môn hóa một phần — bạn sửa một số tham số hoặc hạn chế chúng (ví dụ: chuyên môn hóa cho tất cả các loại con trỏ). Chuyên môn hóa một phần chỉ có sẵn cho các mẫu lớp, không có sẵn cho các mẫu hàm.

Chuyên môn đầy đủ

Chuyên môn hóa đầy đủ cung cấp cách triển khai hoàn toàn tùy chỉnh cho một bộ đối số mẫu cụ thể. Cú pháp sử dụngtemplate <>(danh sách tham số trống) theo sau là lớp hoặc hàm với các đối số cụ thể.

full_specialization.cpp
#include <iostream>
#include <cstring>

// Primary template
template <typename T>
class Formatter {
public:
    static std::string format(const T& value) {
        return std::to_string(value);
    }
};

// Full specialization for const char*
template <>
class Formatter<const char*> {
public:
    static std::string format(const char* value) {
        return std::string("\"" ) + value + "\"";
    }
};

// Full specialization for bool
template <>
class Formatter<bool> {
public:
    static std::string format(bool value) {
        return value ? "true" : "false";
    }
};

int main() {
    std::cout << Formatter<int>::format(42)            << '\n'; // 42
    std::cout << Formatter<const char*>::format("hi")  << '\n'; // "hi"
    std::cout << Formatter<bool>::format(true)          << '\n'; // true
}

Chuyên môn hóa một phần

Chuyên môn hóa một phần hạn chế một số thông số mẫu trong khi vẫn giữ nguyên những thông số chung khác. Điều này cực kỳ mạnh mẽ để khớp các họ loại - tất cả các con trỏ, tất cả các mảng, tất cả các cặp, v.v. Chỉ các mẫu lớp/cấu trúc/biến mới hỗ trợ chuyên môn hóa từng phần.

partial_specialization.cpp
#include <iostream>
#include <string>
#include <typeinfo>

// Primary template
template <typename T>
struct TypeInfo {
    static std::string name() { return "general type"; }
};

// Partial specialization for all pointer types
template <typename T>
struct TypeInfo<T*> {
    static std::string name() {
        return "pointer to " + TypeInfo<T>::name();
    }
};

// Partial specialization for all references
template <typename T>
struct TypeInfo<T&> {
    static std::string name() {
        return "reference to " + TypeInfo<T>::name();
    }
};

// Partial specialization for arrays
template <typename T, std::size_t N>
struct TypeInfo<T[N]> {
    static std::string name() {
        return "array of " + std::to_string(N) + " " + TypeInfo<T>::name();
    }
};

int main() {
    std::cout << TypeInfo<int>::name()     << '\n'; // general type
    std::cout << TypeInfo<int*>::name()    << '\n'; // pointer to general type
    std::cout << TypeInfo<int**>::name()   << '\n'; // pointer to pointer to general type
    std::cout << TypeInfo<int[5]>::name()  << '\n'; // array of 5 general type
}
Lỗi thường gặp

Các mẫu chức năng không thể chuyên biệt một phần - chỉ chuyên biệt hoàn toàn. Tệ hơn nữa, các chuyên môn về mẫu hàm không tham gia vào quá trình giải quyết tình trạng quá tải theo cách bạn mong đợi. Trước tiên, trình biên dịch sẽ chọn mẫu cơ sở tốt nhất thông qua độ phân giải quá tải, sau đó chỉ kiểm tra xem mẫu cơ sở đó có chuyên môn phù hợp hay không.

Điều này dẫn đến hành vi đáng ngạc nhiên:

template <typename T> void f(T)   { /* #1 - primary */ }
template <typename T> void f(T*)  { /* #2 - overload for pointers */ }
template <> void f<int*>(int*)     { /* #3 - specialization of #1 */ }

Calling f((int*)nullptr) selects #2 (the better overload match), not #3 (which specializes #1, not #2). The fix: use regular function overloading instead of function template specialization. If you need compile-time type branching, use if constexpr hoặc các khái niệm.

Mẫu gửi thẻ

Gửi thẻ sử dụng các loại cấu trúc trống ("thẻ") để chọn tình trạng quá tải tại thời điểm biên dịch. Thư viện tiêu chuẩn sử dụng rộng rãi điều này -std::advancegửi đi trên các thẻ danh mục vòng lặp. Đây là kỹ thuật phân nhánh thời gian biên dịch chiếm ưu thế trướcif constexpr.

tag_dispatch.cpp
#include <iostream>
#include <iterator>
#include <vector>
#include <list>

namespace detail {
    // Tags are empty structs used purely for overload selection
    template <typename Iter>
    void advance_impl(Iter& it, int n, std::random_access_iterator_tag) {
        it += n;  // O(1) — random access
        std::cout << "random access advance\n";
    }

    template <typename Iter>
    void advance_impl(Iter& it, int n, std::bidirectional_iterator_tag) {
        // O(n) — step one at a time, forward or backward
        while (n > 0) { ++it; --n; }
        while (n < 0) { --it; ++n; }
        std::cout << "bidirectional advance\n";
    }

    template <typename Iter>
    void advance_impl(Iter& it, int n, std::input_iterator_tag) {
        while (n > 0) { ++it; --n; }
        std::cout << "input iterator advance\n";
    }
}

template <typename Iter>
void my_advance(Iter& it, int n) {
    // iterator_traits extracts the category tag
    detail::advance_impl(it, n,
        typename std::iterator_traits<Iter>::iterator_category{});
}

int main() {
    std::vector<int> v{1, 2, 3, 4, 5};
    auto vit = v.begin();
    my_advance(vit, 3);  // random access advance

    std::list<int> l{1, 2, 3, 4, 5};
    auto lit = l.begin();
    my_advance(lit, 3);  // bidirectional advance
}

if constexpr: Giải pháp thay thế hiện đại

if constexpr của C++17 cung cấp một cách phân nhánh rõ ràng hơn tại thời điểm biên dịch trong một hàm duy nhất. Nhánh bị loại bỏ không được khởi tạo nên không cần phải biên dịch cho kiểu đã cho. Điều này phần lớn thay thế việc gửi thẻ và SFINAE cho các trường hợp đơn giản.

if_constexpr.cpp
#include <iostream>
#include <type_traits>
#include <string>
#include <vector>

template <typename T>
std::string stringify(const T& value) {
    if constexpr (std::is_arithmetic_v<T>) {
        return std::to_string(value);
    } else if constexpr (std::is_same_v<T, std::string>) {
        return '"' + value + '"';
    } else if constexpr (std::is_same_v<T, const char*>) {
        return std::string("\"" ) + value + "\"";
    } else {
        return "[non-printable]";
    }
}

int main() {
    std::cout << stringify(42)              << '\n'; // 42
    std::cout << stringify(3.14)            << '\n'; // 3.140000
    std::cout << stringify(std::string("hello")) << '\n'; // "hello"
    std::cout << stringify("world")         << '\n'; // "world"
}
Thực hành tốt

Sử dụng hướng dẫn quyết định này để phân nhánh thời gian biên dịch:

- if constexpr(C++17) — đơn giản nhất, hoạt động trong một chức năng duy nhất, không cần mẫu đặc biệt. Thích điều này cho logic dựa trên loại đơn giản.
- Khái niệm/yêu cầu (C++20) — tốt nhất để hạn chế quá tải hàm hoặc toàn bộ mẫu. Tự ghi lại và đưa ra thông báo lỗi rõ ràng.
- Gửi thẻ — vẫn hữu ích khi bạn có hệ thống phân cấp thẻ hiện có (như các danh mục lặp). Cơ sở mã trước C++17 phụ thuộc rất nhiều vào điều này.
- SFINAE /enable_if — kỹ thuật lâu đời nhất. Khó đọc hơn, lỗi khó hiểu. Tránh sử dụng mã mới nếu các khái niệm hoặcif constexprhoạt động.
- Chuyên môn hóa đầy đủ — sử dụng cho các mẫu lớp khi bạn cần cách triển khai hoàn toàn khác cho một loại cụ thể.
- Không bao giờ sử dụng chuyên môn hóa mẫu hàm — thay vào đó hãy sử dụng tính năng nạp chồng.

Điểm chính
  • Chuyên môn hóa đầy đủ (template <>) cung cấp triển khai tùy chỉnh cho một loại cụ thể
  • Chuyên môn hóa một phần hạn chế một số tham số - chỉ có sẵn cho các mẫu lớp, không có sẵn cho các mẫu hàm
  • Chuyên môn hóa mẫu chức năng dễ bị lỗi - thích quá tải hoặcif constexpr
  • Gửi thẻ sử dụng các loại cấu trúc trống để chọn tình trạng quá tải tại thời điểm biên dịch thông quaiterator_traits
  • if constexpr(C++17) là giải pháp thay thế hiện đại, gọn gàng hơn giúp loại bỏ các nhánh chưa được sử dụng tại thời điểm biên dịch

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

(20 XP)

1. Tại sao bạn nên tránh các mẫu chức năng chuyên biệt?

2. Which types of templates support partial specialization?

3. What does`if constexpr`do differently from a regular`if`?