Μετάβαση στο περιεχόμενο

C++20 Έννοιες & Περιορισμοί

Ορίστε ακριβείς απαιτήσεις για ορίσματα προτύπων χρησιμοποιώντας έννοιες. Γράψτε πιο καθαρό γενικό κώδικα με ρήτρες απαιτήσεων, συντετμημένα πρότυπα συναρτήσεων και τυπικές έννοιες βιβλιοθήκης.

Οι έννοιες του προβλήματος επίλυση

Πριν από τις έννοιες, τα μηνύματα σφάλματος προτύπου ήταν κρυπτικά — η χρήση τουstd::sortσε έναν τύπο χωρίςoperator<μπορούσε να δημιουργήσει σελίδες κειμένου σφάλματος βαθιά μέσα στην τυπική υλοποίηση βιβλιοθήκης. Οι έννοιες το λύνουν αυτό επιτρέποντάς σας να δηλώνετε απαιτήσεις εκ των προτέρων για τις παραμέτρους προτύπου.

Μια έννοια είναι ένα ονομαζόμενο δυαδικό κατηγόρημα που αξιολογείται κατά το χρόνο μεταγλώττισης. Ελέγχει εάν ένας τύπος ικανοποιεί ένα σύνολο συντακτικών και σημασιολογικών απαιτήσεων. Όταν μια ιδέα δεν ικανοποιείται, ο μεταγλωττιστής παράγει ένα σαφές, συνοπτικό μήνυμα σφάλματος που δείχνει τη μη ικανοποιημένη απαίτηση — όχι έναν τοίχο από backtraces της παρουσίασης προτύπου.

Οι έννοιες εισήχθησαν στο C++20 και αντιπροσωπεύουν τη σημαντικότερη βελτίωση στον γενικό προγραμματισμό από τα ίδια τα πρότυπα.

Καθορισμός εννοιών

Μια έννοια ορίζεται με τη λέξη-κλειδίconceptακολουθούμενη από μια έκφραση απαιτείται που καθορίζει ποιες λειτουργίες πρέπει να υποστηρίζει ένας τύπος. Η έκφραση απαιτεί μπορεί να ελέγξει για έγκυρες εκφράσεις, τύπους επιστροφής, ένθετους τύπους και άλλα.

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
}

Απαιτεί ρήτρες & συντομευμένη σύνταξη

Υπάρχουν πολλοί τρόποι για να εφαρμόσετε μια έννοια σε ένα πρότυπο. Η ρήτρα απαιτείται πηγαίνει μετά τη λίστα παραμέτρων προτύπου ή μετά την υπογραφή της συνάρτησης. Η συντομευμένη σύνταξη προτύπου συνάρτησης χρησιμοποιεί τοConcept autoως τύπο παραμέτρου, που είναι η πιο συνοπτική μορφή.

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
}

Σύνθεση Εννοιών & Πρότυπες Έννοιες

Οι έννοιες συνθέτουν φυσικά με && (σύνδεση) και || (διάζευξη). Η τυπική βιβλιοθήκη παρέχει ένα πλούσιο σύνολο εννοιών στο :

Βασικές γλωσσικές έννοιες:
-std::same_as— Τα T και U είναι του ίδιου τύπου
-std::derived_from— Το παράγωγο προέρχεται από το Base
-std::convertible_to— Το From είναι σιωπηρά μετατρέψιμο σε To
-std::integral,std::floating_point,std::signed_integral

Έννοιες σύγκρισης:
-std::equality_comparable— υποστηρίζει==και!=
-std::totally_ordered— υποστηρίζει<,>,<=,>=,==,!=

Έννοιες αντικειμένων:
-std::movable,std::copyable,std::semiregular,std::regular

Καλήσιμες έννοιες:
-std::invocable— Το F μπορεί να κληθεί με Args
-std::predicate— Το F είναι ένα κατηγόρημα που επιστρέφει bool

Έννοιες επαναληπτικού ():
-std::input_iterator,std::forward_iterator,std::random_access_iterator,std::contiguous_iterator

Έννοιες εύρους ():
-std::ranges::range,std::ranges::sized_range,std::ranges::random_access_range

Έννοια Υπόθεση & Ανάλυση Υπερφόρτωσης

Όταν ταιριάζουν πολλαπλές περιορισμένες υπερφορτώσεις, ο μεταγλωττιστής χρησιμοποιεί υπόθεση έννοιας για να επιλέξει την πιο συγκεκριμένη. Η έννοια Α συνυπολογίζει την έννοια Β εάν οι περιορισμοί του Α υποδηλώνουν λογικά τους περιορισμούς του Β. Αυτό επιτρέπει τη «βελτίωση» — μια πιο συγκεκριμένη έννοια κερδίζει μια πιο γενική.

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
}

Το απαιτεί απαιτεί Μοτίβο

Το κάπως ασυνήθιστο requires requires εμφανίζεται όταν χρησιμοποιείτε μια έκφραση ad-hoc απαιτεί απευθείας σε μια ρήτρα απαίτησης χωρίς να ορίζετε μια ονομασμένη έννοια. Το πρώτοrequiresεισάγει τη ρήτρα περιορισμού. το δεύτεροrequiresξεκινά την έκφραση απαιτεί.

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
}
Βέλτιστη πρακτική

Όταν σχεδιάζετε έννοιες:

- Ονομάστε τις έννοιες μετά από αυτό που είναι ο τύπος, όχι αυτό που χρειάζεται η συνάρτηση — χρησιμοποιήστεSortableόχιHasLessThan. Οι έννοιες πρέπει να συλλαμβάνουν σημασιολογικές κατηγορίες.
- Προτιμήστε τυπικές έννοιες βιβλιοθήκης — προτού γράψετε τις δικές σας, ελέγξτε αν το, τοή τοέχει ήδη αυτό που χρειάζεστε.
- Διατηρήστε τις έννοιες μικρές και συνθέσιμες — συνδυάστε μικρές έννοιες με το&&αντί να δημιουργήσετε μονολιθικές απαιτήσεις.
- Χρησιμοποιήστε επώνυμες έννοιες σε ad-hocrequires requires — η ονομαστική φόρμα είναι πιο ευανάγνωστη και επαναχρησιμοποιήσιμη.
- Τεκμηριώστε τις σημασιολογικές απαιτήσεις στα σχόλια — οι έννοιες ελέγχουν τη σύνταξη και όχι τη σημασιολογία. Εάν θέλετε τοoperator+να είναι ανταλλακτική, πείτε το σε ένα σχόλιο.

Βασικά συμπεράσματα
  • Οι έννοιες ονομάζονται δυαδικά κατηγορήματα που περιορίζουν τις παραμέτρους προτύπου, παρέχοντας σαφή μηνύματα σφάλματος
  • Η έκφρασηrequiresελέγχει για έγκυρες εκφράσεις, τύπους επιστροφής και ένθετες απαιτήσεις
  • Τέσσερις τρόποι εφαρμογής εννοιών: περιορισμός στην παράμετρο προτύπου, ρήτρα απαίτησης, απαίτηση μετάδοσης ήConcept auto
  • Οι έννοιες συνθέτουν με&&και||. Η υπαγωγή επιλέγει την πιο περιορισμένη υπερφόρτωση
  • Η τυπική βιβλιοθήκη παρέχει έννοιες σε,και
  • Τοrequires requiresείναι ένας ad-hoc ενσωματωμένος περιορισμός — προτιμήστε επώνυμες έννοιες για αναγνωσιμότητα

Κουίζ — Δοκιμάστε τις γνώσεις σας

(20 XP)

1. Τι σημαίνει το`void print(std::integral auto value)`;

2. Εάν η έννοια Α ορίζεται ως`concept A = B<T> && C<T>`, τι συμβαίνει όταν ταιριάζουν και μια υπερφόρτωση με περιορισμό Β και μια υπερφόρτωση με περιορισμό Α;

3. Γιατί το`requires requires(T a) { a.size(); }`έχει τη λέξη-κλειδί «απαιτείται» δύο φορές;