Sari la conținut

Vizualizări, evaluare leneșă și compoziție

Vizualizări Master C++20 — adaptoare ușoare, leneșe, care compun cu operatorul de conducte. Învățați să filtrați, să transformați, să preluați, să plasați, să divizați, să uniți, să aflați cum să construiți conducte puternice de date fără alocări intermediare.

Ce sunt vizualizările?

O vizualizare este o gamă ușoară care este:

1. Neposedare — nu deține elementele la care face referire
2. O(1) copiați și mutați — vizualizările sunt ieftine de transmis
3. Evaluate leneș — elementele sunt calculate la cerere în timpul iterației, nu în avans

Vizualizările sunt elementele de bază ale conductelor de gamă. În loc să creeze containere intermediare la fiecare pas, vizualizările compun într-o singură trecere peste date:

// 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::views:: (a namespace alias for std::ranges::views::).

Filtrați și transformați — Vederile fundamentale

views::filterpăstrează elementele care se potrivesc cu un predicat.views::transformaplică o funcție fiecărui element. Împreună, ele înlocuiesc majoritatea buclelor explicite.

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>();
}

luați, plasați și tăiați vizualizările

views::take(n)produce primele n elemente.views::drop(n)omite primele n elemente. Împreună, permit tăierea eficientă fără copiere.

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';
}

Vizualizări împărțite, unite și procesare șiruri

views::splitdivides a range by a delimiter.views::joinflattens nested ranges. These are powerful for string and data processing.

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) { ... }
}

views::iota — Interval de generare

views::iotagenerează o secvență de valori crescătoare. Poate fi mărginit (iota(0, 10)pentru 0..9) sau nemărginit (iota(0)pentru 0, 1, 2, ...). Combinat cutake, înlocuiește multe bucle de numărare.

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
}

Construirea de conducte complete de date

Adevărata putere a vizualizărilor apare atunci când compuneți mai multe adaptoare într-o singură conductă. Întreaga conductă evaluează leneș într-o singură trecere.

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';
    }
}
Capcană

Vizualizările sunt neproprietate - fac referire la intervalul de bază. Dacă domeniul de sursă este distrus, vizualizarea devine atârnând:

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!

Asigurați-vă întotdeauna că datele sursă supraviețuiesc tuturor vizualizărilor care le fac referire. Stocați datele într-o variabilă cu o durată de viață suficientă înainte de a crea vizualizări asupra acestora.

Bună practică

Vizualizările sunt leneșe — nu se lucrează până la iterare. Aceasta înseamnă:

1. Construirea unei conducte este în esență gratuită (stochează doar indicatorii de funcție și parametrii)
2. Puteți crea vizualizări pe domenii foarte mari sau chiar infinite
3. Fiecare element curge prin întreaga conductă înainte ca următorul element să fie procesat
4. Vizualizările în scurtcircuit precumtake(n)opresc întreaga conductă după n elemente

Pentru a materializa rezultatele vizualizării într-un container, repetați și colectați:

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>();

Preferați să păstrați datele ca vizualizări cât mai mult timp posibil - se materializează numai atunci când aveți nevoie de acces aleatoriu, persistență sau pentru a le transmite către API-uri care necesită containere.

Idei cheie
  • Vizualizările sunt adaptoare de gamă care nu sunt proprietare, O(1) copiabile, evaluate leneș
  • views::filterșiviews::transformsunt blocurile fundamentale pentru conductele de date
  • views::take(n)șiviews::drop(n)oferă tăiere fără copiere
  • views::iotagenerează secvențe - mărginite sau nemărginite - înlocuind buclele de numărare
  • Operatorul de conducte|compune vederi în conducte lizibile de la stânga la dreapta cu zero alocări intermediare
  • Vizualizările nu trebuie să depășească datele lor sursă - asigurați-vă întotdeauna că intervalul de bază are o durată de viață suficientă

Test — Verifică-ți cunoștințele

(20 XP)

1. Când își calculează elementele o vizualizare C++20?

2. Care este rezultatul`std::views::iota(1) | std::views::take(3)`?

3. Why is it dangerous to create a view over a local variable and return it from a function?