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

Προβολές, Τεμπέλης Αξιολόγηση & Σύνθεση

Κύρια προβολές C++20 — ελαφρύς, τεμπέλης προσαρμογείς εμβέλειας που συνθέτουν με τον χειριστή του σωλήνα. Μάθετε φιλτράρισμα, μετασχηματισμό, λήψη, απόθεση, διαχωρισμό, σύνδεση, iota και πώς να δημιουργείτε ισχυρούς αγωγούς δεδομένων χωρίς ενδιάμεσες εκχωρήσεις.

Τι είναι οι Προβολές;

Μια προβολή είναι μια ελαφριά περιοχή που είναι:

1. Μη ιδιοκτησία — δεν κατέχει τα στοιχεία στα οποία αναφέρεται
2. O(1) αντιγράψτε και μετακινήστε — οι προβολές είναι φθηνές
3. Αξιολογούνται τεμπέλικα — τα στοιχεία υπολογίζονται κατ' απαίτηση κατά την επανάληψη, όχι εκ των προτέρων

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

// Traditional: creates 2 intermediate vectors
auto evens = filter(data, is_even);     // allocation
auto squared = transform(evens, sq);    // allocation

// Views: zero intermediate allocations
auto result = data | views::filter(is_even) | views::transform(sq);
// Nothing computed yet — result is just a view descriptor
for (int x : result) { ... }  // computed element-by-element

Views live in std::προβολές:: (a namespace alias for std::εύρη::προβολές::).

φιλτράρισμα και μετασχηματισμός — Οι θεμελιώδεις απόψεις

Τοviews::filterδιατηρεί στοιχεία που ταιριάζουν με ένα κατηγόρημα. Τοviews::transformεφαρμόζει μια συνάρτηση σε κάθε στοιχείο. Μαζί, αντικαθιστούν την πλειοψηφία των ρητών βρόχων.

filter_transform.cpp
#include <ranges>
#include <vector>
#include <string>
#include <iostream>

int main() {
    std::vector<int> numbers = {1, 2, 3, 4, 5, 6, 7, 8, 9, 10};

    // Filter even numbers, then square them
    auto even_squares = numbers
        | std::views::filter([](int x) { return x % 2 == 0; })
        | std::views::transform([](int x) { return x * x; });

    // Nothing has been computed yet — even_squares is a view
    for (int x : even_squares) {
        std::cout << x << ' ';  // 4 16 36 64 100
    }
    std::cout << '\n';

    // Views work with any range — including strings
    std::string text = "Hello, World!";
    auto uppercase = text
        | std::views::filter([](char c) { return std::isalpha(c); })
        | std::views::transform([](char c) { return std::toupper(c); });

    for (char c : uppercase) {
        std::cout << c;  // HELLOWORLD
    }
    std::cout << '\n';

    // Collect view results into a vector (C++23: std::ranges::to)
    std::vector<int> collected;
    for (int x : even_squares) {
        collected.push_back(x);
    }
    // C++23: auto collected = even_squares | std::ranges::to<std::vector>();
}

λήψη, απόθεση και κοπή προβολών

Τοviews::take(n)αποδίδει τα πρώτα n στοιχεία. Τοviews::drop(n)παραλείπει τα πρώτα n στοιχεία. Μαζί επιτρέπουν αποτελεσματικό τεμαχισμό χωρίς αντιγραφή.

take_drop.cpp
#include <ranges>
#include <vector>
#include <iostream>

int main() {
    std::vector<int> data = {1, 2, 3, 4, 5, 6, 7, 8, 9, 10};

    // take — first N elements
    for (int x : data | std::views::take(3)) {
        std::cout << x << ' ';  // 1 2 3
    }
    std::cout << '\n';

    // drop — skip first N elements
    for (int x : data | std::views::drop(7)) {
        std::cout << x << ' ';  // 8 9 10
    }
    std::cout << '\n';

    // Combine: elements 3-7 (0-indexed: drop 3, take 4)
    for (int x : data | std::views::drop(3) | std::views::take(4)) {
        std::cout << x << ' ';  // 4 5 6 7
    }
    std::cout << '\n';

    // take_while / drop_while — predicate-based
    std::vector<int> sorted_data = {1, 3, 5, 7, 2, 4, 6};

    auto ascending_prefix = sorted_data | std::views::take_while(
        [prev = 0](int x) mutable {
            bool ok = x > prev;
            prev = x;
            return ok;
        });
    for (int x : ascending_prefix) {
        std::cout << x << ' ';  // 1 3 5 7
    }
    std::cout << '\n';

    // Pagination example: page 2, page_size = 3
    int page = 2, page_size = 3;
    auto page_view = data
        | std::views::drop((page - 1) * page_size)
        | std::views::take(page_size);
    for (int x : page_view) {
        std::cout << x << ' ';  // 4 5 6
    }
    std::cout << '\n';
}

Προβολές διαχωρισμού, σύνδεσης και επεξεργασίας συμβολοσειρών

Τοviews::splitδιαιρεί μια περιοχή με έναν οριοθέτη. Τοviews::joinισοπεδώνει τις ένθετες περιοχές. Αυτά είναι ισχυρά για επεξεργασία συμβολοσειρών και δεδομένων.

split_join.cpp
#include <ranges>
#include <string>
#include <string_view>
#include <vector>
#include <iostream>

int main() {
    // split — divide a string by delimiter
    std::string csv = "Alice,30,Engineer,95000";

    for (auto field : csv | std::views::split(',')) {
        // Each 'field' is a subrange — convert to string_view
        std::string_view sv(field.begin(), field.end());
        std::cout << '[' << sv << "] ";
    }
    std::cout << '\n';  // [Alice] [30] [Engineer] [95000]

    // join — flatten nested ranges
    std::vector<std::vector<int>> matrix = {
        {1, 2, 3},
        {4, 5, 6},
        {7, 8, 9}
    };
    for (int x : matrix | std::views::join) {
        std::cout << x << ' ';  // 1 2 3 4 5 6 7 8 9
    }
    std::cout << '\n';

    // Combine: split lines, then process each line
    std::string multiline = "hello world\nfoo bar\nbaz qux";
    int line_num = 1;
    for (auto line : multiline | std::views::split('\n')) {
        std::string_view line_sv(line.begin(), line.end());
        std::cout << line_num++ << ": " << line_sv << '\n';
    }

    // Enumerate (C++23)
    // for (auto [i, val] : data | std::views::enumerate) { ... }
}

προβολές::iota — Generating Ranges

Τοviews::iotaδημιουργεί μια ακολουθία αυξανόμενων τιμών. Μπορεί να είναι οριοθετημένο (iota(0, 10)για 0..9) ή χωρίς περιορισμό (iota(0)για 0, 1, 2, ...). Σε συνδυασμό με τοtake, αντικαθιστά πολλούς βρόχους μέτρησης.

views_iota.cpp
#include <ranges>
#include <iostream>
#include <vector>
#include <numeric>

int main() {
    // Bounded iota — replaces for(int i = 0; i < 10; ++i)
    for (int x : std::views::iota(0, 10)) {
        std::cout << x << ' ';  // 0 1 2 3 4 5 6 7 8 9
    }
    std::cout << '\n';

    // Unbounded iota + take — first 5 squares
    auto first_5_squares = std::views::iota(1)
        | std::views::transform([](int x) { return x * x; })
        | std::views::take(5);
    for (int x : first_5_squares) {
        std::cout << x << ' ';  // 1 4 9 16 25
    }
    std::cout << '\n';

    // Generate Fibonacci-like sequence with views
    // (iota as index generator)
    auto indices = std::views::iota(0, 20);
    auto even_indices = indices | std::views::filter([](int i) { return i % 2 == 0; });
    for (int i : even_indices | std::views::take(5)) {
        std::cout << i << ' ';  // 0 2 4 6 8
    }
    std::cout << '\n';

    // Use iota to create a vector of sequential values
    auto range_vec = std::views::iota(1, 11);  // 1 to 10
    int sum = 0;
    for (int x : range_vec) sum += x;
    std::cout << "Sum 1..10: " << sum << '\n';  // 55
}

Κατασκευή Ολοκληρωμένων Σωληνώσεων Δεδομένων

Η πραγματική ισχύς των προβολών εμφανίζεται όταν συνθέτετε πολλούς προσαρμογείς σε μια ενιαία γραμμή. Ολόκληρος ο αγωγός αξιολογείται νωχελικά σε ένα μόνο πέρασμα.

data_pipeline.cpp
#include <ranges>
#include <vector>
#include <string>
#include <iostream>
#include <algorithm>

struct LogEntry {
    std::string level;   // "INFO", "WARN", "ERROR"
    std::string message;
    int timestamp;
};

int main() {
    std::vector<LogEntry> logs = {
        {"INFO",  "Server started",      1000},
        {"ERROR", "Connection refused",  1001},
        {"INFO",  "Request received",    1002},
        {"WARN",  "Slow query detected", 1003},
        {"ERROR", "Timeout expired",     1004},
        {"INFO",  "Request completed",   1005},
        {"ERROR", "Disk full",           1006},
        {"WARN",  "Memory usage high",   1007},
    };

    // Pipeline: get recent error messages, formatted
    auto recent_errors = logs
        | std::views::filter([](const LogEntry& e) {
              return e.level == "ERROR";
          })
        | std::views::drop(1)            // skip first error
        | std::views::transform([](const LogEntry& e) {
              return "[" + std::to_string(e.timestamp) + "] " + e.message;
          })
        | std::views::take(5);           // at most 5 results

    std::cout << "Recent errors (after first):\n";
    for (const auto& msg : recent_errors) {
        std::cout << "  " << msg << '\n';
    }
    // [1004] Timeout expired
    // [1006] Disk full

    // Reverse view — iterate backward without copying
    auto reversed = logs | std::views::reverse | std::views::take(3);
    std::cout << "\nLast 3 entries (reversed):\n";
    for (const auto& e : reversed) {
        std::cout << "  [" << e.level << "] " << e.message << '\n';
    }
}
Παγίδα

Οι προβολές δεν ανήκουν — αναφέρονται στο υποκείμενο εύρος. Εάν το εύρος της πηγής καταστραφεί, η προβολή γίνεται κρέμεται:

auto make_view() {
    std::vector<int> local = {1, 2, 3};
    return local | std::views::filter([](int x) { return x > 1; });
    // BUG: local is destroyed, view dangles!
}

Also beware of views over temporaries created mid-pipeline:

// WRONG: the string is a temporary that gets destroyed
auto words = std::string("hello world")
    | std::views::split(' ');  // dangling!

Βεβαιωθείτε ότι τα δεδομένα πηγής διατηρούν διάρκεια όλες τις προβολές που αναφέρονται σε αυτά. Αποθηκεύστε τα δεδομένα σε μια μεταβλητή με επαρκή διάρκεια ζωής πριν δημιουργήσετε προβολές πάνω από αυτήν.

Βέλτιστη πρακτική

Οι προβολές είναι τεμπέλικες — δεν γίνεται δουλειά μέχρι την επανάληψη. Αυτό σημαίνει:

1. Η κατασκευή ενός αγωγού είναι ουσιαστικά δωρεάν (απλώς αποθηκεύει δείκτες συνάρτησης και παραμέτρους)
2. Μπορείτε να δημιουργήσετε προβολές σε πολύ μεγάλες ή ακόμα και άπειρες περιοχές
3. Κάθε στοιχείο ρέει μέσω ολόκληρου του αγωγού πριν από την επεξεργασία του επόμενου στοιχείου
4. Προβολές βραχυκυκλώματος όπως τοtake(n)σταματούν ολόκληρο τον αγωγό μετά από n στοιχεία

Για να υλοποιήσετε τα αποτελέσματα της προβολής σε ένα κοντέινερ, επαναλάβετε και συλλέξτε:

std::vector<int> results;
for (int x : some_view) results.push_back(x);

// C++23 adds std::ranges::to for cleaner materialization:
auto results = some_view | std::ranges::to<std::vector>();

Προτιμήστε να διατηρείτε τα δεδομένα όσο το δυνατόν περισσότερες προβολές — υλοποιούνται μόνο όταν χρειάζεστε τυχαία πρόσβαση, επιμονή ή μεταβίβαση σε API που απαιτούν κοντέινερ.

Βασικά συμπεράσματα
  • Οι προβολές δεν ανήκουν, οι προσαρμογείς εύρους O(1) με δυνατότητα αντιγραφής, νωχελικά αξιολογημένοι
  • Ταviews::filterκαιviews::transformείναι τα θεμελιώδη δομικά στοιχεία για τους αγωγούς δεδομένων
  • Ταviews::take(n)καιviews::drop(n)παρέχουν τεμαχισμό χωρίς αντιγραφή
  • Τοviews::iotaδημιουργεί ακολουθίες — οριοθετημένες ή απεριόριστες — αντικαθιστώντας τους βρόχους μέτρησης
  • Ο χειριστής σωλήνων|συνθέτει προβολές σε ευανάγνωστες σωληνώσεις από αριστερά προς τα δεξιά με μηδενικές ενδιάμεσες εκχωρήσεις
  • Οι προβολές δεν πρέπει να υπερβαίνουν τα δεδομένα πηγής τους — να διασφαλίζετε πάντα ότι το υποκείμενο εύρος έχει επαρκή διάρκεια ζωής

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

(20 XP)

1. Πότε μια προβολή C++20 υπολογίζει τα στοιχεία της;

2. Ποιο είναι το αποτέλεσμα του`std::views::iota(1) | std::views::take(3)`;

3. Γιατί είναι επικίνδυνο να δημιουργήσετε μια προβολή σε μια τοπική μεταβλητή και να την επιστρέψετε από μια συνάρτηση;