Ez az alfejezet egy labor. A célja nem új nyelvi elem bevezetése,
hanem annak begyakorlása, hogy egy generikus felület viselkedését és a
fordítási hibáit a szerződés szintjén tudd leírni. A korábbi részekben
rögzítettük a template-instantiation, a dependent name, a kétfázisú
lookup és az ADL szabályait Dependent name,
kétfázisú lookup és ADL (26.5), valamint a noexcept
határok és a hibapolicy kérdését noexcept határok és
hibapolicy (25.3). Most ezeket egyetlen, kicsi, de életszerű
kódpáron használjuk: egy fix kapacitású, veremtárolón élő pufferen
(SmallBuffer) és egy általános transform
segéden, amely egy bemeneti tartomány elemeit egy kimeneti tartományba
képezi. A labor két kérdésre keresi a választ: mit követelünk meg a
típus- és méretparamétertől, és hogyan feltételezzük a hívási szándékot,
hogy elkerüljük a véletlen másolatokat.
A hangsúly a diagnosztikán van. Egy template-instantiation hibaüzenete első olvasásra ijesztő, de a legtöbbször három dolog egyikére vezethető vissza: a típusparaméter nem teljesíti a szerződést, a hívási kategória nem az, amit a szerző feltételezett, vagy a névfeloldás egy nem kívánt overloadot választott. A laborfeladat az, hogy mindhárom esetre legyen egy minimalizált, reprodukálható példád, és hogy a nyers hibaüzenetet API-feltétellé alakítsd.
A SmallBuffer<T, N> szándékosan szűk: fix,
fordítási időben ismert kapacitás, nincs halomallokáció, az elemek a
SmallBuffer objektumon belül élnek. A
típusparaméter-szerződés a következő.
N > 0, mert nulla kapacitású inline tömb nem
értelmes.T legyen default-inicializálható
(std::default_initializable<T>), mert a
T data_[N]{} tömb mind az N elemet létrehozza,
már a konstrukciókor.T legyen const T&-ről értékadható
(std::assignable_from<T&, const T&>), mert a
push_back a már létező elemre értékadással ír.Ennél többet a SmallBuffer nem igényel: nyers
bájtmásolást nem végez, ezért a triviális másolhatóság nem feltétel. Ez
fontos, mert a labor mérőtípusa, a Counted, szándékosan nem
triviálisan másolható: a másoló és mozgató műveletei számlálnak, és
éppen ezek mérése a labor célja. A három feltételt a
requires klóz rögzíti, így megsértésüket a fordító a
példányosítás helyén jelzi.
A transform ezzel szemben csak azt követeli meg, hogy a
bemenet elemei olvashatók legyenek, a kimenetnek legyen
push_back művelete, a callable pedig legyen hívható a
bemenet elemére. Ez utóbbi feltételt a C++20 requires-szel
fejezzük ki, nem dokumentációval.
// include/small_buffer.hpp
#ifndef SMALL_BUFFER_HPP
#define SMALL_BUFFER_HPP
#include <concepts>
#include <cstddef>
#include <stdexcept>
#include <type_traits>
#include <utility>
template <typename T, std::size_t N>
requires (N > 0) && std::default_initializable<T>
&& std::assignable_from<T&, const T&>
class SmallBuffer {
public:
using value_type = T;
using size_type = std::size_t;
static constexpr size_type capacity() noexcept { return N; }
size_type size() const noexcept { return size_; }
bool empty() const noexcept { return size_ == 0; }
T& operator[](size_type i) noexcept { return data_[i]; }
const T& operator[](size_type i) const noexcept { return data_[i]; }
void push_back(const T& v) {
if (size_ == N) throw std::length_error{"SmallBuffer full"};
data_[size_++] = v;
}
void clear() noexcept { size_ = 0; }
T* begin() noexcept { return data_; }
T* end() noexcept { return data_ + size_; }
const T* begin() const noexcept { return data_; }
const T* end() const noexcept { return data_ + size_; }
private:
T data_[N]{};
size_type size_ = 0;
};
#endifA push_back kapacitáshiba esetén
std::length_error-t dob, tehát nem noexcept. A
transform sem lehet noexcept, hiszen
továbbadja ezt a kivételt; ezt a hibapolicy-szerződés részeként kell
dokumentálni, összhangban noexcept határok és
hibapolicy (25.3)-gyel.
// include/transform.hpp
#ifndef TRANSFORM_HPP
#define TRANSFORM_HPP
#include <functional>
#include <utility>
// A callable-t a ciklusban többször hívjuk, ezért mindig lvalue-ként (fn),
// nem std::forward<F>(fn)-ként: egy rvalue-ra minősített operator() így nem
// "fogyaszthatja el" az állapotát az első hívásnál. A megszorítás pontosan
// ugyanazt az invoke-kifejezést ellenőrzi, amelyet a törzs használ.
template <typename In, typename Out, typename F>
requires requires (const In& in, Out& out, F& fn) {
in.begin(); in.end();
std::invoke(fn, *in.begin()); // hívható az elemmel?
out.push_back(std::invoke(fn, *in.begin())); // a kimenet fogadja?
}
void generic_transform(const In& in, Out& out, F&& fn) {
for (const auto& x : in) {
out.push_back(std::invoke(fn, x));
}
}
#endifA bemenet const In&, mert csak olvassuk; ez nem
deduction-, hanem interfész-döntés. A callable F&&
forwarding reference, mert így semmilyen callable nem másolódik az
átvételkor, legyen az lambda, állapotot hordozó funktor vagy
tagadat-pointer. A törzsben viszont a callable-t
lvalue-ként (fn) hívjuk, mert a ciklus
többször hívja; a std::forward<F>(fn) ismételt
használata egy &&-minősített
operator() esetén már az első hívásnál „elfogyaszthatná” az
állapotot. A megszorítás ugyanazt az std::invoke(fn, ...)
kifejezést ellenőrzi ugyanazzal az értékkategóriával
(F& fn, const elem), amit a törzs használ,
két külön követelményként: hívható-e a callable az elemmel, és fogadja-e
a kimenet az eredményt. Így a megszorítás és a ciklustörzs ugyanazt a
felületet modellezi, és a std::invoke miatt
tagadat-pointeres projekció (&Counted::v) is
működik.
A forwarding reference lényege, hogy a dedukált T nem a
paraméter típusa, hanem egy „nyers” típus, amelyből a
referencia-kollapszus szabálya állítja elő a tényleges paramétert. A
laborban ezt a következő táblázattal rögzítjük; a séma
template<class T> void f(T&&).
| Hívás | Dedukált T |
Paraméter típusa |
|---|---|---|
int x; f(x); |
int& |
int& |
f(7); |
int |
int&& |
const int cx; f(cx); |
const int& |
const int& |
f(std::move(cx)); |
const int |
const int&& |
f("lit"); |
const char (&)[4] |
const char (&)[4] |
Három következményt érdemes kiemelni. Először, T lvalue
esetén referenciává válik, tehát a T&& alak nem
jelenti azt, hogy a hívó csak rvalue-t adhat át. Másodszor, a
const minősítő a dedukált típusba kerül, és nem tűnik el:
f(cx) esetén T = const int&. Harmadik, a
std::move(cx) nem rvalue-vé alakítja a cx-et,
csak const int&&-t ad, tehát a
transform nem tudja „elrabolni” a tartalmat; ez a
klasszikus const-move csapda.
A generic_transform hívásakor a mátrix két helyen
játszik: a bemeneti és kimeneti argumentumnál, valamint a callable-nél.
A const In& alak miatt a bemenet lvalue marad. A
callable-nél a dedukció a hívó szándékát rögzíti (lambda-temporaryra
F a lambdatípus, lvalue funktorra F
lvalue-referencia), de a törzs a callable-t szándékosan lvalue-ként
hívja, mert többször használja; a std::forward<F>(fn)
csak egyetlen, utolsó használatnál lenne indokolt. Ha
std::ref-fel burkolt funktort ad, a
reference_wrapper másolása olcsó.
A leglátványosabb hiba nem fordítási, hanem teljesítménybeli: a helper véletlenül másol. Ezt egy számláló típussal tesszük láthatóvá.
// include/counted.hpp
#ifndef COUNTED_HPP
#define COUNTED_HPP
#include <cstddef>
struct Counted {
static inline std::size_t copies = 0;
static inline std::size_t moves = 0;
int v{};
Counted() = default;
explicit Counted(int x) : v{x} {}
Counted(const Counted& o) : v{o.v} { ++copies; }
Counted(Counted&& o) noexcept : v{o.v} { ++moves; }
Counted& operator=(const Counted& o) { v = o.v; ++copies; return *this; }
Counted& operator=(Counted&& o) noexcept { v = o.v; ++moves; return *this; }
~Counted() = default;
};
#endifA driver két variánst hasonlít össze: egy
transform_by_value segédet, amely a callable-t érték
szerint veszi át, és a generic_transform-et, amely
forwarding referenciát használ.
// src/main.cpp
#include "small_buffer.hpp"
#include "transform.hpp"
#include "counted.hpp"
#include <cstdio>
#include <utility>
template <typename In, typename Out, typename F>
void transform_by_value(const In& in, Out& out, F fn) {
for (const auto& x : in) out.push_back(fn(x));
}
// Külön instrumentált callable: a saját számlálója csak a funktor
// másolásait/mozgatásait méri, a Counted-számlálótól függetlenül.
struct CountingFn {
static inline std::size_t copies = 0;
static inline std::size_t moves = 0;
CountingFn() = default;
CountingFn(const CountingFn&) { ++copies; }
CountingFn(CountingFn&&) noexcept { ++moves; }
Counted operator()(const Counted& c) const { return Counted{c.v * 2}; }
};
int main() {
SmallBuffer<Counted, 8> in;
SmallBuffer<Counted, 8> out;
for (int i = 0; i < 4; ++i) in.push_back(Counted{i});
out.clear();
auto twice = [](const Counted& c) { return Counted{c.v * 2}; };
// 1. mérés: elemek másolása (Counted-számláló), állapot nélküli lambda.
Counted::copies = Counted::moves = 0;
transform_by_value(in, out, twice);
std::printf("by_value: copies=%zu moves=%zu size=%zu\n",
Counted::copies, Counted::moves, out.size());
out.clear();
Counted::copies = Counted::moves = 0;
generic_transform(in, out, twice);
std::printf("forwarding: copies=%zu moves=%zu size=%zu\n",
Counted::copies, Counted::moves, out.size());
// 2. mérés: a callable másolása (CountingFn-számláló).
CountingFn fn;
out.clear();
CountingFn::copies = CountingFn::moves = 0;
transform_by_value(in, out, fn);
std::printf("fn by_value: copies=%zu moves=%zu\n",
CountingFn::copies, CountingFn::moves);
out.clear();
CountingFn::copies = CountingFn::moves = 0;
generic_transform(in, out, fn);
std::printf("fn forwarding: copies=%zu moves=%zu\n",
CountingFn::copies, CountingFn::moves);
// Tagadat-pointer mint projekció: std::invoke(&Counted::v, x) == x.v
SmallBuffer<int, 8> ids;
generic_transform(in, ids, &Counted::v);
std::printf("projection: size=%zu last=%d\n", ids.size(), ids[ids.size() - 1]);
}A fordítás és a futtatás külön laborlépés:
mkdir -p build
g++ -std=c++20 -Wall -Wextra -Wpedantic -Iinclude \
src/main.cpp src/reader_a.cpp src/reader_b.cpp src/small_buffer_inst.cpp \
-o build/lab
./build/labA labor teljes fájlleltára: include/small_buffer.hpp,
include/transform.hpp, include/counted.hpp,
include/small_buffer_extern.hpp, src/main.cpp,
src/reader_a.cpp, src/reader_b.cpp,
src/small_buffer_inst.cpp, valamint a negatív tesztek a
test/ könyvtárban (neg_small_buffer.cpp,
neg_callable.cpp).
Buildjegyzőkönyv (lab-gcc profil). A
labor figyelmeztetés nélkül fordult; a kimenet:
by_value: copies=4 moves=0 size=4
forwarding: copies=4 moves=0 size=4
fn by_value: copies=1 moves=0
fn forwarding: copies=0 moves=0
projection: size=4 last=3
A két mérés két külön számlálót olvas. Az elemek
másolása (Counted-számláló): mindkét változatban
4, és ez a várt, elfogadott eredmény. A twice lambda
állapot nélküli, ezért az érték szerinti átvétele egyetlen
Counted-et sem másol; a négy másolás a négy kimeneti elem
push_back-értékadásából jön
(data_[size_++] = v, ahol v
const Counted&). A callable másolása
(CountingFn-számláló): az érték szerinti
transform_by_value lvalue fn-ből pontosan egy
másolatot készít (copies=1), a forwarding-referenciás
generic_transform egyet sem. A kvalitatív elvárás tehát: a
callable-másolás a CountingFn számlálóján jelenik meg, és
nem keveredik az elemek másolásával. Ha a lambda érték szerint elkapott
Counted-et hordozna, az egy callable-másolás a
Counted-számlálón is +1-ként jelentkezne a négy elemmásolás
mellett — ezért mérünk külön funktorral.
A negatív tesztek (test/neg_callable.cpp,
-DNEG_NOT_CALLABLE, illetve
-DNEG_NO_PUSH_BACK) mindkettő
constraints not satisfied hibával bukik. Nem hívható
objektumnál a jegyzet az első követelményt nevezi meg
(the required expression 'std::invoke(fn, (* in.begin()))' is invalid),
push_back nélküli kimenetnél csak a másodikat
('out.push_back(std::invoke(fn, (* in.begin())))' is invalid).
// test/neg_callable.cpp -- szándékosan nem fordul
#include "small_buffer.hpp"
#include "transform.hpp"
struct NoPushBack { };
struct NotCallable { };
int main() {
SmallBuffer<int, 4> in;
SmallBuffer<int, 4> out;
#if defined(NEG_NOT_CALLABLE)
generic_transform(in, out, NotCallable{});
#elif defined(NEG_NO_PUSH_BACK)
NoPushBack sink;
generic_transform(in, sink, [](int x) { return x; });
#endif
}Érdemes megfigyelni, hogy a generic_transform belsejében
a for (const auto& x : in) miatt a callable argumentuma
const Counted&, nem másolat. Ez a
push_back által végzett másolást nem spórolja meg, mert a
SmallBuffer::push_back a kimenetbe ír. Ha a
push_back-ot emplace_back-kel és a callable
visszatérési értékének közvetlen átadásával váltanánk ki, a konstruktor
egy lépésben futna le.
A template definíciójának a headerekben kell élnie, mert az
instantiation a fordító dolga, nem a linkeré. Ha a
transform.hpp-t két .cpp is beemeli, mindkét
fordítási egységben példányosodnak ugyanazok a sablonok. Ez nem
ODR-sértés, mert a template-ek (és az osztályon belül definiált
tagfüggvények) implicit inline jelölésűek, és a szabvány
megengedi, hogy több fordítási egységben azonos definíció
szerepeljen.
// src/reader_a.cpp
#include "transform.hpp"
#include "small_buffer.hpp"
#include "counted.hpp"
void run_a(SmallBuffer<Counted, 8>& in, SmallBuffer<Counted, 8>& out) {
generic_transform(in, out, [](const Counted& c) { return Counted{c.v + 1}; });
}// src/reader_b.cpp
#include "transform.hpp"
#include "small_buffer.hpp"
#include "counted.hpp"
void run_b(SmallBuffer<Counted, 8>& in, SmallBuffer<Counted, 8>& out) {
generic_transform(in, out, [](const Counted& c) { return Counted{c.v - 1}; });
}A két generic_transform különböző F
típusokkal példányosul, tehát két külön szimbólum keletkezik. Ugyanarra
az F-re viszont ugyanaz a weak szimbólum jön létre mindkét
egységben, és a linker egyet választ. Ezt a
nm -C build/reader_a.o kimenetén ellenőrizheted (a
reader_a.o a
g++ -std=c++20 -Iinclude -c src/reader_a.cpp -o build/reader_a.o
paranccsal készül). A lab-gcc profilban a
SmallBuffer<Counted, 8ul> tagfüggvényei, például a
push_back, W (weak) jelölést kapnak. A
generic_transform példánya viszont t
(lokális): a lambda típusa a fordítási egységhez kötött, ezért az ilyen
F-fel készült példány nem is lehet közös a két egység
között.
Nagy projektekben a sok azonos instantiation lassítja a fordítást. A
template class SmallBuffer<Counted, 8>; explicit
instantiation egy fordítási egységben kényszeríti a példányosítást, az
extern template class SmallBuffer<Counted, 8>; pedig
a többi egységben elnyomja az implicit példányosítást.
// src/small_buffer_inst.cpp
#include "small_buffer.hpp"
#include "counted.hpp"
template class SmallBuffer<Counted, 8>; // kényszerített példány// include/small_buffer_extern.hpp
#pragma once
#include "small_buffer.hpp"
#include "counted.hpp"
extern template class SmallBuffer<Counted, 8>; // máshol példányosulA trade-off három ponton mérhető: fordítási idő, objektumfájl mérete,
linkelési idő. Az explicit instantiation akkor nyer, ha ugyanazt a
sablont sok fordítási egység használja, és a
T/N kombinációk zárt halmaza ismert. Hátránya,
hogy a felhasználónak gondoskodnia kell arról, hogy az
extern template deklaráció minden olyan egységben látható
legyen, ahol az implicit példányosítást el akarja nyomni; ha kimarad, a
példányosítás visszatér, és nem kapsz hibát, csak lassabb buildet.
A negatív fordítási teszt egy olyan forrás, amelynek szándékosan nem
szabad lefordulnia, és amelynek a hibaüzenetét ellenőrizzük. A cél az,
hogy a hibaüzenet egy static_assert vagy
requires feltételre vezethető legyen vissza, ne egy mélyen
beágyazott STL-instantiation láncra.
A SmallBuffer szerződéséhez két negatív típus tartozik,
egy-egy megsértett feltétellel:
// test/neg_small_buffer.cpp -- szándékosan nem fordul
#include "small_buffer.hpp"
struct NoDefault { // nincs default konstruktor
explicit NoDefault(int) {}
};
struct ConstMember { // const tag: nem értékadható
const int v = 0;
};
#ifdef NEG_ASSIGN
using Bad = SmallBuffer<ConstMember, 4>;
#else
using Bad = SmallBuffer<NoDefault, 4>;
#endif
int main() {
Bad b;
(void)b;
}A requires klóz miatt a fordító a hibaüzenetben a
SmallBuffer deklarációját és a klózt nevezi meg, alatta
pedig a ténylegesen megsértett feltételt; nem az STL belsejére mutat.
Mivel a klóz az osztálysablon deklarációján áll, egy osztályon belüli
static_assert meg sem jelenne: a példányosítás már a klóz
ellenőrzésén elbukik. Saját szövegű üzenethez a feltételt nevesített
conceptbe lehet kiemelni.
A teszt futtatása shell-szintű; mindkét változatnak meg kell buknia, és mindkettőnek a saját okával:
mkdir -p build
if g++ -std=c++20 -Iinclude -fsyntax-only test/neg_small_buffer.cpp 2>build/diag1.txt; then
echo "FAIL: expected compile error (NoDefault)"; exit 1
fi
grep -q "_Tp = NoDefault" build/diag1.txt
if g++ -std=c++20 -Iinclude -DNEG_ASSIGN -fsyntax-only test/neg_small_buffer.cpp 2>build/diag2.txt; then
echo "FAIL: expected compile error (ConstMember)"; exit 1
fi
grep -q "__lhs = static_cast" build/diag2.txtBuildjegyzőkönyv (lab-gcc profil).
Mindkét változat template constraint failure hibával
bukott. A NoDefault esetén a jegyzet a
default_initializable alatti
is_constructible_v<_Tp, _Args ...> [with _Tp = NoDefault; _Args = {}]
kifejezést nevezi meg, a ConstMember esetén az
assignable_from
__lhs = static_cast<_Rhs&&>(__rhs)
követelményét. A grep minták a libstdc++ conceptjeinek
szövegéhez kötöttek; más fordítóhoz igazítani kell őket.
Egy másik tipikus negatív eset a forwardoló segédnél lép fel: ha a
generic_transform-et olyan típussal hívjuk, amelynek nincs
push_back művelete, a requires feltétel hamis
lesz, és a hiba a hívás helyére mutat. Ha viszont a
requires-t elhagynánk, a hiba a transform.hpp
belsejébe esne, és a hívó számára követhetetlen lenne. A
requires tehát nem esztétikai, hanem diagnosztikai
eszköz.
A leggyakoribb minta, hogy a szerző T-t érték szerint
veszi át a callable-nél (F fn), és ezzel minden híváskor
másol. A javítás F&& átvétel; a hívás a törzsben
std::invoke(fn, ...), lvalue-ként, ha a callable-t többször
hívjuk. A std::forward<F>(fn) csak az utolsó
(egyetlen) használatnál helyes.
A második minta a const T&& félreértése:
std::move(cx) után a T const int,
tehát a T&& paraméter nem teszi lehetővé az
elrablást. A javítás: ha tényleg mozgatni akarunk, a
const-ot már a hívó oldalon el kell hagyni.
A harmadik minta az explicit instantiation és az
extern template sorrendjének felcserélése: ha az
extern template deklaráció után következik egy implicit
használat, a fordító a deklarációt figyelmen kívül hagyhatja, és a
példányosítás újra megtörténik. Az extern template
deklarációnak minden olyan egységben láthatónak kell lennie, ahol a
példányosítást el akarod nyomni.
A labor két dolgot mutatott meg. Először, a generikus interfész
szerződése több szinten él: a SmallBuffer-nél a típus- és
méretparaméterre vonatkozó
requires/static_assert, a
generic_transform-nél a bemenet és a kimenet műveleteire
vonatkozó requires. Másodszor, a hívási kategória és a
forwarding nem stílus kérdése: a Counted számlálóval
láthatóvá tehető, hol keletkezik felesleges másolat, és a mátrix alapján
megjósolható, mit fog tenni a std::forward. A header Több
TU-ban élő template-jei ODR-biztosak, az explicit instantiation építési
időért cserébe optimalizál, a negatív fordítási teszt pedig akkor jó, ha
a hibaüzenet a saját szerződésünkre mutat.
1. feladat (magyarázó). Egészítsd ki a
const/lvalue/rvalue mátrixot a generic_transform hívására:
SmallBuffer<Counted,8> buf; generic_transform(buf, out, [](auto x){ return x; });
esetén mi lesz In, Out, F
dedukált típusa, és miért hívja a törzs a callable-t lvalue-ként, nem
std::forward<F>(fn)-nel? Indokold a választ a
referencia-kollapszus szabályával.
2. feladat (kódolási). Módosítsd a
generic_transform-et úgy, hogy opcionálisan egy
reserve-szerű előkészítést is végezzen a kimeneten, ha az
támogatja a reserve műveletet, egyébként ne tegyen semmit.
A megoldás legyen C++20-kompatibilis, és ne regresszálja a meglévő
requires feltételt.
3. feladat (diagnosztikai). Írj negatív fordítási
tesztet arra az esetre, amikor a callable visszatérési típusa nem
konvertálható a kimenet value_type-jára, és fogalmazd meg a
requires feltételt úgy, hogy a hibaüzenet a
transform.hpp-ra és a hívásra mutasson, ne az STL
belsejére. Add meg az ellenőrző shell-parancs vázát is.
1. megoldás. A buf lvalue
SmallBuffer<Counted,8>, a const In&
paraméterhez In = SmallBuffer<Counted,8>. Az
out szintén lvalue,
Out = SmallBuffer<Counted,8>. A lambda-kifejezés
prvalue, tehát F a lambda típusa (nem referencia), az
fn paraméter típusa F&&. A
std::forward<F>(fn) ezt xvalue-ként adná tovább; a
ciklusban azonban a callable-t négyszer hívjuk, és egy
&&-minősített hívóoperátor az első hívásnál
elhasználhatná az állapotát. Ezért a törzs
std::invoke(fn, x)-et hív, ahol fn lvalue, és
a megszorítás is F& fn-nel ellenőriz. A
const In& miatt a bemenet nem módosítható, és a
for (const auto& x : in) miatt az x is
const Counted&; az auto x paraméter ezt
lemásolja, tehát ez a feladat szándékosan bemutat egy accidental copy-t,
amit auto const&-re cserélve lehet megszüntetni.
2. megoldás. A requires feltételt
bővíteni kell egy opcionális ággal, de a reserve hiánya ne
tegye hamissá a feltételt. C++20-ban erre a requires
feltétel nem alkalmas közvetlenül; egy külön
if constexpr (requires { out.reserve(0); }) blokk a
függvénytestben oldja meg:
template <typename In, typename Out, typename F>
requires requires (const In& in, Out& out, F& fn) {
in.begin(); in.end();
std::invoke(fn, *in.begin());
out.push_back(std::invoke(fn, *in.begin()));
}
void generic_transform(const In& in, Out& out, F&& fn) {
if constexpr (requires { out.reserve(in.size()); }) {
out.reserve(in.size());
}
for (const auto& x : in) {
out.push_back(std::invoke(fn, x));
}
}Az if constexpr ága csak akkor példányosul, ha a
reserve hívás jól formált; ellenkező esetben a blokk
eldobásra kerül, és nem okoz hibát. Ellenőrzési feltétel: a
SmallBuffer (amelynek nincs reserve-je)
változatlanul fordul, és a std::vector-ral hívott változat
is.
3. megoldás. A requires feltételbe be
kell venni a visszatérési típus konvertálhatóságát. A kimenet típusát a
decltype(*out.begin()) adja meg:
template <typename In, typename Out, typename F>
requires requires (const In& in, Out& out, F& fn) {
in.begin(); in.end();
requires std::convertible_to<
std::invoke_result_t<F&, decltype(*in.begin())>,
std::remove_cvref_t<decltype(*out.begin())>>;
out.push_back(std::invoke(fn, *in.begin()));
}
void generic_transform(const In& in, Out& out, F&& fn);A negatív tesztnél a callable egy olyan típust ad vissza, amelyből
nincs konverzió a kimenet value_type-jára. Az ellenőrző
parancs:
if g++ -std=c++20 -Iinclude -fsyntax-only test/neg_transform.cpp 2>build/diag.txt; then
echo "FAIL"; exit 1
fi
grep -q "constraints not satisfied" build/diag.txtA grep minta fordítófüggő; a lényeg, hogy a diagnosztika
a generic_transform deklarációjára és a hívás helyére
mutasson.