A labor két, a gyakorlatban gyakran összekapcsolódó szerződést vizsgál: a lazy ranges pipeline és a callable-ek élettartamát. A cél nem az, hogy a lazy view-t automatikusan biztonságosnak vagy cache-eltnek tekintsük, hanem hogy minden példában megnevezzük a tulajdonost, a view élettartamhatárát, az invalidáló műveletet és a hiba megjelenési formáját. Az előfeltételek: a borrowed range és a dangling részletes tárgyalása a Borrowed range, dangling és iterator lifetime (30.5) részben, a callable és type erasure alapjai az invoke, function wrapper és type erasure (30.2) részben, az ASan és társai az ASan, UBSan, MSan, TSan és Valgrind (16.2) részben találhatók. A fejezet a C++20 alapra épít; a C++23-as lehetőségeket külön jelöljük, és a választott compiler/library feature-testjével kell ellenőrizni őket. A kódokra a fejezet nem állítja, hogy lefuttattuk őket: a várt kimenet, a mérési terv és az ASan-lelet a tanuló laborjához tartozik.
A lazy view olyan range, amely nem tárolja az
elemeit, hanem a mögöttes range-ből számítja ki őket; a
begin() és end() által visszaadott iterátorok
az inputtól függenek. A filter_view és a
transform_view C++20-ban nem tulajdonolják a mögöttes
tartományt, kivéve, ha az egy owning_view. A
std::views::all lvalue range-re ref_view-ot,
rvalue range-re owning_view-ot ad; a ref_view
nem tartja életben a mögöttes objektumot, az owning_view
igen.
A borrowed_range olyan range, amelynek iterátorai a
range objektum élettartamától függetlenül érvényesek maradhatnak. A
std::string_view és a std::span jellemzően
borrowed range, a std::vector nem. Ez azonban nem azonos a
tulajdonlással: a std::vector nem borrowed range, mégis
tulajdonol, ezért a belőle materializált kimenet biztonságos. A
dangling olyan pointer, referencia vagy view, amelynek
hivatkozott objektuma már nem él; a használata undefined behavior
(UB).
A materialization (anyagiasítás) a lazy view
elemeinek kiértékelése és egy tulajdonló konténerbe másolása vagy
mozgatása. A callable (hívható objektum)
std::invoke-bal hívható dolog: lambda closure,
függvénypointer, tagfüggvénypointer vagy függvényobjektum. A closure
copyable vagy move-only volta a
capture-öktől függ; a std::unique_ptr capture move-only
closure-t eredményez. A capture (befogás) lehet érték
szerinti, referencia szerinti vagy init-capture; referencia szerint nem
hosszabbítja meg a temporary élettartamát.
A labor központi szerződése táblázatban:
| Objektum | Tulajdonol? | Élettartam | Invalidálja |
|---|---|---|---|
std::vector<LogRecord> logs |
igen | a logs scope-ja |
megszűnés, reallokáció |
error_view(logs) |
nem | a logs-hoz kötött |
a logs megszűnése vagy módosítása |
materialize_errors(logs) visszatérési értéke |
igen | a visszaadott vector | a visszaadott vector megszűnése |
std::string_view a logs elemére |
nem | a logs elemhez kötött |
az elem megszűnése vagy módosítása |
A szerződés kimondása önmagában nem véd meg: a
[[nodiscard]] és a const nem akadályozza meg a
danglinget. A static_assert a
std::ranges::borrowed_range concepttel dokumentálhatja,
hogy egy visszaadott view nem borrowed range, de a helyes használat
továbbra is a hívó felelőssége.
A példa egy naplófájl-szerű rekordhalmazt szűr és vetít. A
LogRecord tulajdonosa a std::vector; a
pipeline a vektorra hivatkozik. A filter predikátuma a
rekord szintjét vizsgálja, a transform a
message mezőre vetít. A vetítés a ranges algoritmusok
projection paraméterének mintájára működik: a
std::ranges::sort és a std::ranges::find_if is
kaphat tagfüggvény-pointert vagy lambdát.
#include <algorithm>
#include <chrono>
#include <iostream>
#include <iterator>
#include <ranges>
#include <string>
#include <vector>
struct LogRecord {
std::chrono::system_clock::time_point ts;
std::string level;
std::string message;
};
std::vector<LogRecord> load_demo_logs() {
using namespace std::chrono_literals;
const auto base = std::chrono::system_clock::now();
return {
{base + 1s, "INFO", "service started"},
{base + 2s, "ERROR", "connection refused"},
{base + 3s, "WARN", "retrying"},
{base + 4s, "ERROR", "timeout"},
};
}
std::vector<std::string> materialize_errors(const std::vector<LogRecord>& logs) {
auto view = logs
| std::views::filter([](const LogRecord& r) { return r.level == "ERROR"; })
| std::views::transform([](const LogRecord& r) -> const std::string& {
return r.message;
});
std::vector<std::string> out;
out.reserve(logs.size());
std::ranges::copy(view, std::back_inserter(out));
return out;
}
auto error_view(const std::vector<LogRecord>& logs) {
return logs
| std::views::filter([](const LogRecord& r) { return r.level == "ERROR"; })
| std::views::transform([](const LogRecord& r) -> const std::string& {
return r.message;
});
}
int main() {
auto logs = load_demo_logs();
for (const auto& msg : materialize_errors(logs)) {
std::cout << "materialized: " << msg << '\n';
}
auto view = error_view(logs);
for (const std::string& msg : view) {
std::cout << "lazy: " << msg << '\n';
}
}A materialize_errors visszatérési értéke tulajdonol: a
std::string elemek másolatai a logs-tól
függetlenül élnek. Az error_view ezzel szemben nem
tulajdonol: a visszaadott view a logs-ra hivatkozik, ezért
a hívónak a view használatáig életben kell tartania a vektort. A
std::ranges::copy azért kell, mert a
filter_view begin() és end()
típusa eltérhet (a sentinel std::default_sentinel_t lehet),
a C++20-as std::vector(first, last) pedig azonos iterator
típust vár. A C++23-as std::ranges::to és a
std::vector range-konstruktora ezt kényelmesebben kezelné,
de ezek C++23-as lehetőségek.
A buildparancs a laborban például:
g++ -std=c++20 -O2 -Wall -Wextra -Wpedantic -g loglab.cpp -o loglab
./loglabA várt kimenet az ERROR szintű üzeneteket tartalmazza,
de ezt a fejezet nem mérési eredményként, hanem elvárásként rögzíti. A
sort projection használata külön ellenőrizhető:
std::ranges::sort(logs, std::ranges::less{}, &LogRecord::level);
auto it = std::ranges::find_if(
logs,
[](const std::string& level) { return level == "ERROR"; },
&LogRecord::level);A projection itt a &LogRecord::level; a
find_if predikátuma már a vetített értéket kapja.
A következő két példa szándékosan hibás; a fejezet nem futtatja őket, és a tanuló laborjában is csak izolált, jelölt kísérletként használhatók.
// NEGATÍV PÉLDA: dangling view lokális vectorra. Ne futtasd!
auto bad_error_view() {
auto logs = load_demo_logs();
return logs
| std::views::filter([](const LogRecord& r) { return r.level == "ERROR"; })
| std::views::transform([](const LogRecord& r) -> const std::string& {
return r.message;
});
}A logs lokális std::vector, a
views::all lvalue-ra ref_view-ot ad, a
filter_view és a transform_view ezt tárolja. A
függvényből való kilépéskor a logs megszűnik; a visszaadott
view dangling. A javítás lehet a std::move(logs) használata
a pipe-ban, ami C++20-ban owning_view-ot eredményez, vagy a
materialization, vagy a vector kívülről való átadása.
// NEGATÍV PÉLDA: temporaryre mutató referencia capture. Ne futtasd!
auto make_level_filter(const std::string& level) {
return [&level](const LogRecord& r) { return r.level == level; };
}
auto f = make_level_filter("ERROR"); // a temporary a kifejezés végén megszűnikA "ERROR"-ból ideiglenes std::string
keletkezik, a const std::string& ehhez kötődik, a
closure pedig referenciát capture-öl. A temporary a teljes kifejezés
végén megszűnik, így f hívásakor a level
dangling. A helyes változat érték szerint capture-öl:
auto make_level_filter(std::string level) {
return [level = std::move(level)](const LogRecord& r) {
return r.level == level;
};
}A closure a saját std::string másolatát tartalmazza,
ezért a temporary megszűnése után is biztonságos, és a closure copyable
marad. A hiba nyelvi oka a referencia élettartam-szerződés megsértése;
az ASan a stack-use-after-scope vagy
heap-use-after-free leletet adhatja, de a lelet típusa
implementáció- és allokációfüggő, és a hiánya nem bizonyíték a
helyességre.
A C++20-as std::function copy-constructible callable-t
vár. Egy std::unique_ptr-t capture-ölő lambda move-only
closure, ezért nem tehető std::function-ba:
#include <functional>
#include <memory>
auto make_counting_predicate() {
auto count = std::make_unique<std::size_t>(0);
return [count = std::move(count)](const LogRecord& r) {
++*count;
return r.level == "ERROR";
};
}
// C++20: std::function<bool(const LogRecord&)> f = make_counting_predicate(); // hibaA std::function példányosítása static_asserttel utasítja
el a nem copy-constructible callable-t; a pontos üzenet
implementációfüggő. C++20-ban két egyszerű megoldás van. Az egyik a
template paraméter, amely nem tárolja el a callable-t, hanem
átveszi:
template <class Pred>
std::vector<LogRecord> filter_logs(const std::vector<LogRecord>& logs, Pred pred) {
std::vector<LogRecord> out;
for (const auto& r : logs) {
if (pred(r)) out.push_back(r);
}
return out;
}A hívás filter_logs(logs, std::move(pred)) formában
move-only predikátummal is működik, mert a paraméter érték szerinti, és
a függvényen belül nem kell másolni. A másik megoldás egy saját,
move-only type-erasure wrapper
std::unique_ptr<Concept>-tel; ez C++20-ban is
megírható, de több kódot igényel. C++23-ban a
std::move_only_function erre kész megoldás, használatát
azonban külön kell jelölni és a library supporttal ellenőrizni. A ranges
adaptorok callable-követelménye adaptoronként és verziónként eltér,
ezért nem általánosítható:
views::filter: a predikátumnak
std::indirect_unary_predicate-et kell modelleznie, amely
copy_constructible-t is megkövetel — C++20-ban és C++23-ban
is. Egy unique_ptr-t rögzítő predikátummal a view létre sem
jön.views::transform: C++20-ban a
függvényobjektumnak másolhatónak kell lennie; C++23-ban (P2494,
__cpp_lib_ranges >= 202207L) elég a mozgathatóság.A filter-hez ezért másolható predikátum kell. Ha az
állapot move-only, a predikátumot egy nevesített, a view-nál tovább élő
változóban tartjuk, és std::ref-fel adjuk át; a view így
nem birtokolja az állapotot, csak hivatkozik rá:
auto limit = std::make_unique<int>(1);
auto pred = [l = std::move(limit)](int x) { return x > *l; };
auto r = v | std::views::filter(std::ref(pred)); // pred-nek túl kell élnie r-tBuildjegyzőkönyv. A közvetlen
unique_ptr-capture-ös predikátummal a
views::filter a lab-gcc profilban
(-std=c++20 és -std=c++23) és Clang 22 /
libc++ alatt (mindkét módban) is fordítási hibát adott; a
std::ref-es változat mindenhol lefordult. A move-only
views::transform Clang 22 / libc++ alatt
-std=c++23-ban lefordult, a GCC 13.3 libstdc++-a
(__cpp_lib_ranges == 202202L) viszont még elutasította,
mert a P2494-et nem implementálja.
A lazy pipeline létrehozása nem járja be a tartományt, ezért ; egy bejárás a filter predikátumát minden elemre meghívja, , a transform a kimeneti elemekre . A materializálás ugyanezt egyszer elvégzi, majd a kimenetet tárolja: idő és memória; a későbbi bejárás . Ha a pipeline-t többször járjuk be, a lazy változat minden alkalommal újraszűr, a materializált nem. A lazy view nem cache-el automatikusan; a „lazy” nem jelent sem biztonságot, sem gyorsítótárazást. A döntés: egyszeri bejárásnál és memóriaérzékeny esetben a lazy előnyös; többszöri bejárásnál vagy stabil kimenetnél a materialization a jobb.
A mérés a laborban történik, azonos inputtal, compilerrel és optimalizálási profillal. Vázlat:
template <class F>
double time_ms(F&& f) {
const auto t0 = std::chrono::steady_clock::now();
f();
const auto t1 = std::chrono::steady_clock::now();
return std::chrono::duration<double, std::milli>(t1 - t0).count();
}A mért blokkban a lazy view-t és a materializált vektort
ugyanannyiszor kell bejárni, és az eredményt el kell használni (például
volatile összegzéssel vagy kiírással), hogy a compiler ne
hagyja el a számítást. A mérés eredménye gép-, compiler- és
optimalizálásfüggő; a fejezet nem állít konkrét számot.
A negatív példa izolált vizsgálata például:
g++ -std=c++20 -g -O1 -fsanitize=address -fno-omit-frame-pointer bad.cpp -o bad_asan
./bad_asanAz ASan lelet lehet heap-use-after-free vagy
stack-use-after-return; a riport a hibás hozzáférést és az
érintett allokációt mutatja. A nyelvi ok ettől külön dokumentálandó: a
bad_error_view egy ref_view-ot tartalmaz,
amely a lokális logs-ra mutat; a logs
élettartama a függvény végén véget ér, a view nem tulajdonol, ezért a
használat UB. Az ASan egy implementációs diagnosztikai eszköz, nem a
szabvány; nem jelez minden UB-t, és a „nincs lelet” nem bizonyítja a
helyességet. A laborjegyzőkönyvben ezért külön rubrika legyen a leletre
és külön a nyelvi élettartam-szerződésre.
std::function-ba move-only lambda-t tenni
C++20-ban.std::vector(view.begin(), view.end())-del próbálni, holott
a sentinel típusa eltérhet; helyette std::ranges::copy
kell.Adott a következő paraméter:
const std::vector<LogRecord>& logsÍrj függvényt, amely a WARN szintű rekordok üzeneteit
std::vector<std::string> típusú értékként adja
vissza. Magyarázd el, ki tulajdonol, és meddig él a belső view.
Írj make_level_filter függvényt, amely egy szintet
capture-öl, és visszaad egy closure-t. Készíts hozzá egy negatív
változatot is referencia szerinti capture-rel, és indokold a
különbséget.
Készíts olyan predikátumot, amely egy
std::unique_ptr<std::size_t> számlálót tart.
Tárold/Add át egy szűrőfüggvénynek C++20-ban. Miért nem megy
std::function-nal, és mi a C++23-as lehetőség?
A bad_error_view példához írj egy rövid diagnosztikai
jegyzőkönyvet: mi az ASan lelet, mi a nyelvi ok, és mi a javítás?
Készíts olyan javított változatot, amely nem támaszkodik a lokális
vektorra.
A függvény a logs-ra hivatkozó
filter_view-ot és transform_view-ot lokálisan
használja, majd std::ranges::copy-val másol a kimeneti
vektorba. A kimeneti vektor tulajdonol; a belső view élettartama a
függvényhívásra korlátozódik, a logs a hívóé. Ellenőrzés: a
visszaadott vektor elemei a WARN üzenetek, és a
logs módosítása után a kimenet változatlan.
A helyes változat std::string paramétert és
[level = std::move(level)] capture-t használ. A closure a
saját másolatát birtokolja, ezért a hívási kifejezés temporaryje
megszűnése után is jó. A negatív változat
const std::string& paramétert és
[&level] capture-t használ; a temporary a teljes
kifejezés végén megszűnik, a closure dangling. Ellenőrzés: a helyes
closure "ERROR" temporaryvel létrehozva is helyes eredményt
ad; a negatív változat ASan alatt leletet adhat, de a nyelvi ok
önmagában is UB.
A std::function C++20-ban copy-constructible callable-t
követel, a unique_ptr-t capture-ölő lambda nem az. A
template paraméteres filter_logs(logs, std::move(pred))
megoldás move-only predikátumot is átvesz, mert a paraméter érték
szerinti és nem kell másolni. C++23-ban a
std::move_only_function használható, de ezt jelölni kell és
feature-testtel ellenőrizni. Ellenőrzés: a template-es változat fordul
-std=c++20-szal; a std::function-ös változat
fordítási hibát ad; a számláló a várt számot mutatja.
Az ASan lelet jellemzően heap-use-after-free vagy
stack-use-after-return a visszaadott view bejárásakor. A
nyelvi ok: a ref_view a lokális logs-ra mutat,
a logs a függvény végén megszűnik, a view nem owning, ezért
a hozzáférés UB. A javítás lehet a std::move(logs) a
pipe-ban (owning_view), a materialization, vagy a vector
kívülről való átadása. Ellenőrzés: a javított változat ASan alatt nem
jelez; a következő static_assert dokumentálja, hogy a
visszaadott view nem borrowed range.
static_assert(!std::ranges::borrowed_range<
decltype(error_view(std::declval<const std::vector<LogRecord>&>()))>);A lazy ranges pipeline nem tulajdonol, kivéve az
owning_view-ot; a visszaadott view szerződése mindig
kimondandó. A materialization tulajdonló kimenetet ad, és a többszöri
bejárásnál cache-ként viselkedik, a lazy view nem. A capture élettartama
a capture módjától függ; a referencia capture nem hosszabbítja meg a
temporary élettartamát. A std::function C++20-ban copyable
callable-t vár; move-only callable-hez template, saját wrapper vagy
C++23-as std::move_only_function kell. Az ASan lelet és a
nyelvi élettartam-ok külön dokumentálandó; a diagnosztika hiánya nem
bizonyíték a helyességre.