Chuyển đến nội dung

C++20 Khái niệm & ràng buộc

Xác định các yêu cầu chính xác về đối số mẫu bằng cách sử dụng các khái niệm. Viết mã chung rõ ràng hơn với các mệnh đề yêu cầu, mẫu hàm viết tắt và các khái niệm thư viện tiêu chuẩn.

Các khái niệm vấn đề Giải quyết

Trước khi có khái niệm, thông báo lỗi mẫu rất khó hiểu — sử dụngstd::sorttrên loại không cóoperator<có thể tạo ra các trang có văn bản lỗi sâu bên trong quá trình triển khai thư viện tiêu chuẩn. Các khái niệm giải quyết vấn đề này bằng cách cho phép bạn nêu các yêu cầu lên trước trên các tham số mẫu.

khái niệm là một vị từ boolean được đặt tên được đánh giá tại thời điểm biên dịch. Nó kiểm tra xem một kiểu có thỏa mãn một tập hợp các yêu cầu về cú pháp và ngữ nghĩa hay không. Khi một khái niệm không được thỏa mãn, trình biên dịch sẽ tạo ra một thông báo lỗi rõ ràng, ngắn gọn chỉ ra yêu cầu không được thỏa mãn — chứ không phải một bức tường các vết lùi khởi tạo mẫu.

Các khái niệm đã được giới thiệu trong C++20 và thể hiện sự cải tiến đáng kể nhất đối với lập trình chung kể từ chính các mẫu.

Xác định khái niệm

Một khái niệm được xác định bằng từ khóaconcept, theo sau là biểu thức requires chỉ định những thao tác mà một loại phải hỗ trợ. Biểu thức yêu cầu có thể kiểm tra các biểu thức hợp lệ, kiểu trả về, kiểu lồng nhau, v.v.

concept_definition.cpp
#include <concepts>
#include <iostream>
#include <string>

// Define a concept: T must support +, ==, and copy construction
template <typename T>
concept Addable = requires(T a, T b) {
    { a + b } -> std::convertible_to<T>;  // a+b must return something convertible to T
    { a == b } -> std::convertible_to<bool>; // must be comparable
    requires std::copy_constructible<T>;     // nested requirement
};

// Concept for types that have .size() and .empty()
template <typename T>
concept Sizable = requires(const T& t) {
    { t.size() } -> std::convertible_to<std::size_t>;
    { t.empty() } -> std::convertible_to<bool>;
};

// Use the concept to constrain a template
template <Addable T>
T sum_three(T a, T b, T c) {
    return a + b + c;
}

template <Sizable Container>
void report_size(const Container& c) {
    std::cout << "Size: " << c.size()
              << (c.empty() ? " (empty)" : "") << '\n';
}

int main() {
    std::cout << sum_three(1, 2, 3)       << '\n'; // 6
    std::cout << sum_three(1.5, 2.5, 3.0) << '\n'; // 7.0

    std::string s = "hello";
    report_size(s);  // Size: 5
    std::vector<int> v{1, 2, 3};
    report_size(v);  // Size: 3
}

Mệnh đề yêu cầu & Cú pháp viết tắt

Có nhiều cách để áp dụng một khái niệm vào một mẫu. Mệnh đề requires đặt sau danh sách tham số mẫu hoặc sau chữ ký hàm. Cú pháp mẫu hàm viết tắt sử dụngConcept autolàm loại tham số, đây là dạng ngắn gọn nhất.

requires_clause.cpp
#include <concepts>
#include <iostream>

// Method 1: Concept as template parameter constraint
template <std::integral T>
T gcd(T a, T b) {
    while (b != 0) {
        T temp = b;
        b = a % b;
        a = temp;
    }
    return a;
}

// Method 2: requires clause after template parameters
template <typename T>
    requires std::integral<T>
T lcm(T a, T b) {
    return (a / gcd(a, b)) * b;
}

// Method 3: Trailing requires clause
template <typename T>
T absolute(T value) requires std::signed_integral<T> {
    return value < 0 ? -value : value;
}

// Method 4: Abbreviated function template (Concept auto)
void print_integral(std::integral auto value) {
    std::cout << "Integral: " << value << '\n';
}

// Concept auto works for return types too (C++20)
std::integral auto compute() {
    return 42;  // must return an integral type
}

int main() {
    std::cout << gcd(12, 8)       << '\n'; // 4
    std::cout << lcm(12, 8)       << '\n'; // 24
    std::cout << absolute(-42)    << '\n'; // 42
    print_integral(100);                    // Integral: 100
    // print_integral(3.14);  // ERROR: constraint not satisfied
}

Thành Phần Ý Tưởng & Ý Tưởng Tiêu Chuẩn

Các khái niệm được kết hợp một cách tự nhiên với && (liên từ) và || (liên từ). Thư viện chuẩn cung cấp một bộ khái niệm phong phú trong :

Khái niệm ngôn ngữ cốt lõi:
-std::same_as— T và U cùng loại
-std::derived_from— Có nguồn gốc từ Base
-std::convertible_to— Từ hoàn toàn có thể chuyển đổi thành To
-std::integral,std::floating_point,std::signed_integral

Khái niệm so sánh:
-std::equality_comparable- hỗ trợ==và!=
-std::totally_ordered— hỗ trợ<,>,<=,>=,==,!=

Khái niệm đối tượng:
-std::movable,std::copyable,std::semiregular,std::regular

Khái niệm có thể gọi được:
-std::invocable— F có thể được gọi bằng Args
-std::predicate— F là biến vị ngữ trả về bool

Khái niệm về trình vòng lặp ():
-std::input_iterator,std::forward_iterator,std::random_access_iterator,std::contiguous_iterator

Khái niệm về phạm vi ():
-std::ranges::range,std::ranges::sized_range,std::ranges::random_access_range

Khái niệm gộp & giải quyết quá tải

Khi nhiều tình trạng quá tải bị ràng buộc khớp nhau, trình biên dịch sẽ sử dụng tổng hợp khái niệm để chọn một tình trạng quá tải cụ thể nhất. Khái niệm A bao gồm khái niệm B nếu các ràng buộc của A hàm ý một cách hợp lý các ràng buộc của B. Điều này cho phép "sàng lọc" - một khái niệm cụ thể hơn sẽ chiến thắng khái niệm tổng quát hơn.

subsumption.cpp
#include <concepts>
#include <iostream>

// Concept hierarchy: sorted_range is "more specific" than range
template <typename T>
concept Printable = requires(std::ostream& os, const T& t) {
    { os << t };
};

template <typename T>
concept PrintableNumber = Printable<T> && std::integral<T>;

// Less constrained overload
template <Printable T>
void display(const T& value) {
    std::cout << "[generic] " << value << '\n';
}

// More constrained overload — subsumes Printable
template <PrintableNumber T>
void display(const T& value) {
    std::cout << "[number] " << value << '\n';
}

int main() {
    display(42);              // [number] 42 — PrintableNumber is more specific
    display(std::string("hi")); // [generic] hi — only Printable matches
    display(3.14);            // [generic] 3.14 — not integral
}

Các yêu cầu đòi hỏi mẫu

requires requires hơi bất thường xuất hiện khi bạn sử dụng biểu thức yêu cầu đặc biệt trực tiếp trong mệnh đề yêu cầu mà không xác định khái niệm được đặt tên.requiresđầu tiên giới thiệu mệnh đề ràng buộc;requiresthứ hai bắt đầu biểu thức yêu cầu.

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

// Ad-hoc constraint without naming a concept
template <typename T>
    requires requires(T a, T b) {
        { a + b } -> std::same_as<T>;
    }
T add(T a, T b) {
    return a + b;
}

// Equivalent named concept (preferred for readability)
template <typename T>
concept AddableToSelf = requires(T a, T b) {
    { a + b } -> std::same_as<T>;
};

template <AddableToSelf T>
T add_named(T a, T b) {
    return a + b;
}

int main() {
    std::cout << add(1, 2)                << '\n'; // 3
    std::cout << add(std::string("a"), std::string("b")) << '\n'; // ab
    std::cout << add_named(10, 20)        << '\n'; // 30
}
Thực hành tốt

Khi thiết kế các khái niệm:

- Đặt tên các khái niệm theo loại IS, không phải chức năng cần gì — sử dụngSortablechứ không phảiHasLessThan. Các khái niệm nên nắm bắt các phạm trù ngữ nghĩa.
- Ưu tiên các khái niệm thư viện tiêu chuẩn — trước khi viết thư viện của riêng bạn, hãy kiểm tra xem,hoặcđã có thứ bạn cần chưa.
- Giữ cho các khái niệm nhỏ gọn và có thể kết hợp được — kết hợp các khái niệm nhỏ với&&thay vì tạo ra các yêu cầu nguyên khối.
- Sử dụng các khái niệm được đặt tên trênrequires requiresđặc biệt — biểu mẫu được đặt tên dễ đọc và tái sử dụng hơn.
- Yêu cầu ngữ nghĩa của tài liệu trong nhận xét — cú pháp kiểm tra khái niệm, không kiểm tra ngữ nghĩa. Nếu bạn yêu cầuoperator+có tính giao hoán, hãy nói như vậy trong phần nhận xét.

Điểm chính
  • Các khái niệm được đặt tên là các vị từ boolean hạn chế các tham số mẫu, cung cấp thông báo lỗi rõ ràng
  • Biểu thứcrequireskiểm tra các biểu thức hợp lệ, kiểu trả về và các yêu cầu lồng nhau
  • Bốn cách để áp dụng các khái niệm: ràng buộc về thông số mẫu, mệnh đề yêu cầu, yêu cầu theo sau hoặcConcept auto
  • Các khái niệm kết hợp với&&và||; phép gộp chọn mức quá tải bị hạn chế nhất
  • Thư viện chuẩn cung cấp các khái niệm trong,và
  • requires requireslà một ràng buộc nội tuyến đặc biệt - ưu tiên các khái niệm được đặt tên để dễ đọc

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

(20 XP)

1. `void print(std::integral auto value)`có nghĩa là gì?

2. Nếu khái niệm A được định nghĩa là`concept A = B<T> && C<T>`, điều gì sẽ xảy ra khi cả quá tải ràng buộc B và quá tải ràng buộc A đều khớp nhau?

3. Tại sao`requires requires(T a) { a.size(); }`có từ khóa 'yêu cầu' hai lần?