Gabriel Nützi, 2018 at Cyfex AG, Zurich
[Teil 1] [Teil 1 Bemerkungen] [Teil 2]
[Git Repo]
[unabh. der Sprache, Steve McConnel, Code Complete]
if constexpr(1 < 3 + 4)
{
/* compile this */
}
else
{
/* compile this */
}
The next big Thing [Andrei Alexandrescu, Presentation, Video]
"A common fallacy is to assume authors of incomprehensible code will somehow be able to express themselves lucidly and clearly in comments."
How to write more reliable code [Egor Bredikhin, Presentation, Video]
How to write more reliable code [Egor Bredikhin, Presentation, Video]
Wieso
mySuperSort(v);
anstatt
std::sort(begin(v), end(v), std::greater<>());
"Unless you are an expert in sorting algorithms and have plenty of time, this is more likely to be correct and to run faster than anything you write for a specific application. You need a reason not to use the standard library rather than a reason to use it."
How to write more reliable code [Egor Bredikhin, Presentation, Video]
class ConditionChecker
{
public:
virtual bool CheckCondition() const = 0;
}
Clean Coders Hate When You Use These Tricks [Kevlin Henney, Video]
- Generel: Expliziter is besser ⟶ aber am richtigen Ort:
class SelectionManager
{
public:
class TriangulationSceneObjectProxyWrapper {/* Stellvertr. */};
TriangulationSceneObjectProxyWrapper&
GetCurrentSelectedTriangulationObjectProxyWrapper();
};
Besser:
public:
class TriObjectProxy { ... };
TriObjectProxy& GetSelectedTriangulation();
}
Clean Coders Hate When You Use These Tricks [Kevlin Henney, Video]
Building a C++ Reflection System [Arvid Gerstman, Presentation, Video]
Type t = user.GetType();
-
Suche im Abstract Syntax Tree (AST) mit einem Clang Tool (LLVM).
-
Annotierte Klassen:
struct __attribute__((annotate("reflect"))) User {...}
-
Annotierte Klassen:
- 2-3 Tage Aufwand ⟶ Serialisierung
Compile-time programming and reflection in C++20... [Louis Dionne, Video]
Vor C++11:
Binden eines temporären Objekts an eine non-const
Referenz:
int& a = 0;
⟶ Compile Error 🚫.
non-const lvalue reference to type 'int' cannot bind
to a temporary of type 'int'
Ab C++11: Neue Syntax für Referenzen auf temporäre Objekte:
int&& a = 0; // rvalue-Referenz auf `int`.
Jede Variabel besitzt ein Typ.
int a;
enum class C {A, B, C} b;
std::vector<int> c; // Variable `c`: `std::vector<int>`
int const * & const d;
using FuncPointer = int (*)(float); // Typ Def.: Funktions-Pointer.
FuncPointer pFunc;
decltype(a)
liefert den Typ der Variablea
template <typename T,...> std::vector
ist kein Typ!
⟶ Template (siehe Teil 2)
const
,volatile
*
,&
gehören generell zum Typ und sind Modifikatoren.
Deshalb nicht zur Variabel:
const int *&d;
😩
Wichtig, vorallem bei Templates ⟶ Teil 2.
int *e, f; // Gilt nicht als Ausrede (eher eine C++ Exception)
Jede Expression expr
besitzt zusätzlich zum Typ auch eine Value-Kategorie:
const int& b = 3;
int c = b;
// ^------ Expr. Typ `const int`
Der Typ einer Expression ist nie eine Referenz weil Referenzen entfernt werden für jede weitere Analyse. Siehe Regel [7.2.2#1]
struct A{ A& operator+(const A&); };
A a, b;
a = a+b;
// ^^^------ Expr. Typ ??
a+b
ist A
!
Jede Expression expr
besitzt zusätzlich zum Typ eine Value-Kategory:
const int& b = 3;
int c = b;
// ^------ Expr. Typ `const int`
Grundlegende Kategorien [basic.lval]
Klassifiziert ein Objekt wessen Ressource nicht wiederverwendet werden kann. Alles von welchem man die Adresse nehmen kann.
int y; // Variabel-Typ: 'int'
int* p = &y; // Variabel-Typ: 'int*'
int& r = y; // Variabel-Typ: 'int&'
int&& rr = y; // Variabel-Typ: 'int&&'
Expressions y
, p
, r
, rr
sind alles lvalues.
- Alle Variablen sind immer lvalue.
Grundlegende Kategorien [basic.lval]
Klassifiziert ein Objekt wessen Ressource wiederverwendet werden darf. Meist weil am Ende seiner Laufzeit. Das beinhaltet temporäre erstellte Objekte (Materialisierung).
Banane make(){ return Banane{}; };
- Expression
Banane{}
⟶ TypBanane
und rvalue
Banane& a = make();
- Expression:
make()
: TypBanane
und rvalue - Variable
a
: TypBanane&
- Compile Error 🚫 :
Banane&
bindet nicht an temporäreBanane
.
Syntax | Namen | Verhalten |
---|---|---|
Banane& |
lvalue-Referenz | Bindet lvalues. |
const Banane& |
const-lvalue-Referenz | Bindet lvalue und rvalue (weil const). [Ext. Life-Time] |
Banane&& |
rvalue-Referenz | Bindet nur rvalues. |
T&& [Template-Parameter T ] oder auto&& |
forwarding-Referenz | Bindet an rvalue/lvalues. Um Value-Kategorie zu erhalten |
Beispiel 1 [Live]
int&& a = 3; // 1. Value-Kategorie von `a`?
int& b = a; // 2. Value-Kategorie von `b`?
// 3. Kompiliert das?
- Variable
a
hat Typint&&
[rvalue-Referenz aufint
].
Expression(a)
ist lvalue mit Typ:int
. - Variable
b
hat Typint&
[lvalue-Referenz aufint
].
Expression(b)
ist lvalue mit Typ:int
. - Ja es kompiliert! 👌 .
Value-Kategorie bei Rückgabewerten [expr.call]
int& lvalue(); lvalue(); // Expression ist lvalue;
int&& xvalue(); xvalue(); // Expression ist xvalue;
int prvalue(); prvalue(); // Expression ist prvalue;
Value-Kategorie bei Expression f()
[expr.call]
Rückgabetyp von f() |
Expression f() [Typ, Value-Kategorie] |
|
---|---|---|
Banane |
⟶ | [Banane , prvalue] |
Banane& |
⟶ | [Banane , lvalue] |
Banane&& |
⟶ | [Banane , xvalue] |
decltype(expr)
:
decltype(expr) |
Expression expr |
|
---|---|---|
Banane |
⟵ | [Banane , prvalue] |
Banane& |
⟵ | [Banane , lvalue] |
Banane&& |
⟵ | [Banane , xvalue] |
int& lvalue(); int&& xvalue(); int prvalue();
int& r = lvalue(); // 👌: lvalue-Ref. bindet an lvalue.
int& r = prvalue(); // 🚫: bindet nicht an prvalue.
int& r = xvalue(); // 🚫: bindet nicht an xvalue.
const int& r = prvalue(); // 👌: bindet an prvalue.
const int& r = xvalue(); // 👌: bindet an xvalue.
int&& rr = lvalue(); // 🚫: rvalue-Ref. bindet nicht an lvalue.
int&& rr = prvalue(); // 👌: bindet an prvalue.
int&& rr = xvalue(); // 👌: bindet an xrvalue.
- Bindet nur an rvalues!
class Banane
{
public:
Banane(const Banane& rOther); // Copy-Konstruktor
Banane(Banane&& rrOther); // Move-Konstruktor;
Banane& operator=(const Banane& rOther); // Assign-Operator
Banane& operator=(Banane&& rrOther); // Move-Assign-Operator;
};
- Effizienter "Copy"-Konstruktor möglich ⟶ Move-Konstruktor.
- Effizienter "Assign"-Operator möglich ⟶ Move-Assign-Operator.
Banane create();
Banane a = create();
-
create()
liefert prvalue mit Expr. TypBanane
. -
Der Move-Konstruktor (falls es ihn gibt) wird dem Copy-Construktor bevorzugt,
weil
Banane&&
besser matched.
Benutze decltype((expr))
:
T
fallsexpr
ein prvalue ist.T&
fallsexpr
ein lvalue ist.T&&
fallsexpr
ein xvalue ist.
Wieso (expr)
: Weil explizit so beschrieben in [dcl.type.decltype]
int a;
decltype((a))::GUGUS // Kompilierfehler 🚫
error: 'decltype((a))' (aka 'int &') is not a class,
namespace, or enumeration
- ⟶
a
ist lvalue.
Transformiert die Value-Kategorie einer Expression von lvalue nach xvalue.
struct A{}; A a; // 'a' ist lvalue.
std::move(a); // ... eine xvalue-Expression.
xvalue()
.
A&& std::move(...) { return static_cast<A&&>(a) };
Somit ist std::move(a)
ein xvalue vom Expr. Typ:
A
,
welches zu Aufruf
A(A&& a); // Move-Konstruktor.
A& operator=(A&& a); // Move-Zuweisungsoperator.
führen kann was dann Move heisst:
A b = std::move(a); // ⟶ Move-Konstruktor
class MpAlgo
{
public:
Triangulation& GetOutput() { return m_Tri; };
};
MpAlgo algo;
Triangulation r1 = algo.GetOutput(); // 1. Typ/Val.Kat?
Triangulation r2 = std::move(algo.GetOutput()); // 2. Typ/Val.Kat?
// 3. m_Tri?
-
Expr. Typ
Triangulation
und lvalue ⟶ Copy-Konstruktor. -
Expr. Typ
Triangulation
und xvalue ⟶ Move-Konstruktor. m_Tri
ist in einem undefinierten Zustand (nach Standard)- Aufpassen bei Wiederverwendung von
algo
.
Array<int> createIds() {
Array<int> ids;
for(int i=0; i<42; ++i){
ids.AddTail(i);
}
return ids;
};
Array<int> ids = createIds(); // 1) Typ / Value-Kategorie
createIds()
:Array<int>
und prvalue ⟶ Move-Konstruktor falls vorhanden sonst Copy-Konstruktor.
Return Value Optimzation (RVO) ist verpflichtend seit C++17
Array<int> createIds() {
return {1, 2, 3, 4};
};
Array<int> ids = createIds(); // 1.
Teil 2: Template Refresher, Ref. Collapsing, std::forward
, Template Argument Deduktion, C++17, Code Simplify
etc. (vielleicht auch Teil 3 ?)
- [C++2x Standard]
- [C++ Meeting 2018 Slides]
- [Value Categories in C++17]
- [Effective Modern C++, 2015]
- Kapitel 6. Rvalue-Referenzen, Move-Semantik und Perfect-Forwarding.
- [C++ Templates: The Complete Guide]
- Appendix B: Value Categories. Empfehlenswert. IMO: Diese Slides fassen das aber besser zusammen.
Für die Anwendung reicht es aus zwischen rvalue und lvalue zu unterscheiden:
std::move(banane)
ergibt eine Expression mit Value-Kategorie xvalue:
⟶ "kann gemoved werden"
d.h. bei Konstruktion oder Zuweisung matcht Move-Konstruktor/Move-Assigment besser. Fallback ist immer Copy-Konstruktor/Copy-Assigment.
Typen von Expressions sind nie Referenzen: [1], [2], [2], [3]
Nur [Venn-Diagram] für C++17 wichtig!
Recap 1 : [Named]-Return-Value-Optimization [Live]
Es geht um prvalue
s in einer Return-Expression:
struct A {
A(const A&) = delete;
A(const A&&) = delete;
};
A nrvo() { A a; return a; };
A rvo() { return A{}; };
A rvo2() { return rvo(); };
A a = nrvo(); // NRVO optional , kompiliert nicht, weil `=delete`.
A b = rvo(); // RVO verpflichtend, kompiliert auch ohne Move/Copy.
A c = rvo2(); // RVO verpflichtend, kompiliert auch ohne Move/Copy.
- C++17 guarantiert copy-elision für RVO Fälle wie
rvo()
,rvo2()
.
Moven heisst Move-Konstruktor/Assignment aufrufen.
Objekte die billig zu moven sind
- Value-Semantik: Return/Call-By-Value
- Generell nie
return std::move(ret)
benutzen:A create() { A ret; ... return ret; }; // NRVO.
Objekte die teuer zu moven sind, z.B.
Triangulation
:- Eher Referenz-Semantik: Return/Call-By-Reference
Recap 2 [Live]
struct Banane{};
void shake(const Banane& rB);
void shake(Banane&& rrB);
Banane a;
a; // 1. Typ/Val.Kat von Expression `a` ?
Banane b = std::move(a); // 2. Rückgabe-Typ von `std::move(a)` ?
// 3. Typ/Val.Kat von `std::move(a)`?
// 4. Was ist der Zustand von `a` ?
shake(b); // 5. Welche Funktion wird aufgerufen?
shake(std::move(b)); // 6. Welche Funktion wird aufgerufen?
-
Expression
a
ist lvalue und TypBanane
. -
Rückgabetyp von
std::move(b)
istBanane&&
. -
Expression
std::move(b)
ist xvalue und TypBanane
. -
a
ist gemoved,a
ist in unspezifiziertem aber validen Zustand.
Man sollte nichts anderes annehmen, auch wenn kopiert wurde in 2. -
shake(const Banane&)
. -
shake(Banane&&)
matcht das xvalue vonstd::move(b)
besser.
Recap 3 [Live]
struct Banane{};
void foo(const Banane& rB);
void foo(Banane&& rrB);
Banane a;
Banane&& b = std::move(a); // 1. Was ist der Zustand von `a` ?
foo(b); // 2. Welche Funktion wird aufgerufen?
foo(std::move(b)); // 3. Welche Funktion wird aufgerufen?
-
Der Speicher von
a
hat sich nicht geändert! -
foo(const Banane&)
. Achtung:b
is lvalue!! -
foo(Banane&&)
matcht das xvalue vonstd::move(b)
besser.
struct A
{
A(const A& rOther){
m_v = rOther.m_v;
}
A(A&& rrOther){
m_v = rrOther.m_v; // 1. Wieso ist das hier suboptimal?
}
std::vector<int> m_v;
};
-
Expression
rrOther
ist lvalue, d.h. hier wird kopiert. Die VariablerrOther
ist aber eine Referenz auf ein temporäresA
[rvalue-Reference].
Richtig wäre:std::move(rrOther.m_v)
damit das retournierte xvalue (std::vector<int>&&
) den Move-Assign-Operator matcht.
struct A{};
A& get() { return A{}; } // No!!!
A& get() { A a; return a; } // No!!!
const A& get() { return A{}; } // No!!!
A&& get() { return A{}; } // No!!!
A&& get() { A a; return std::move(a); } // No!!!
auto& get() { return A{}; } // No!!!
auto&& get() { return A{}; } // No!!!
- Wichtig: Referenzen zurückgeben auf lokale temporäre Objekte ist immer quatsch! Compiler sollte warnen!
A get() { return A{}; }
A get() { return A{}; }
auto get() { return A{}; } // kein auto& oder auto&& !!
Recap 6 - Aus Best Practice Chat [Live]
void SetData(A& a);
A a;
SetData(std::move(a)); // Darf nicht kompilileren! 🚫:
- Eine lvalue-Referenz bindet nicht an
rvalues
. MSVC
erlaubt das trotzdem [Live] obwohl nicht-standard konform. ⟶ 💩
if (auto val = GetValue(); condition(val))
{
// ...
}
if (const auto it = myString.find("Hello");
it != std::string::npos)
{
std::cout << it << " Hello\n";
}
- Scope verkleinern.
Structured Bindings [Live]
std::unordered_map<int,std::string> map;
if (auto [it, bSuccess] = map.emplace(3, "Banane"); bSuccess)
{
std::cout << it->second; // dereferezieren
}
// `it` and `bSuccess` sind hier destruktiert.
namespace A::B::C {
//...
}
Compile-Time Switches:
template<typename ElementRef>
void compute(ElementRef ref)
{
if constexpr(std::is_same_v<ElementRef, CVertexRef>) {
// kompiliere das, was speziell ist für CVertexRef
}
else if constexpr(std::is_same_v<ElementRef, TriangleRef>) {
// kompiliere das, was speziell ist für TriangleRef
}
else {
static_assert(false, "Not-implemented!");
}
}
Es geht anscheinend noch schwieriger: MUMPS
%DTC ; SF/XAK - DATE/TIME OPERATIONS ;1/16/92 11:36 AM
;;19.0;VA FileMan;;Jul 14, 1992
D I 'X1!'X2 S X="" Q
S X=X1 D H S X1=%H,X=X2,X2=%Y+1 D H S X=X1-%H,%Y=%Y+1&X2
K %H,X1,X2 Q
;
C S X=X1 Q:'X D H S %H=%H+X2 D YMD S:$P(X1,".",2)
S S %=%#60/100+(%#3600\60)/100+(%\3600)/100 Q
;
H I X<1410000 S %H=0,%Y=-1 Q
S %Y=$E(X,1,3),%M=$E(X,4,5),%D=$E(X,6,7)
S %T=$E(X_0,9,10)*60+$E(X_"000",11,12)*60+$E(X_"00000",13,14)
TOH S %H=%M>2&'(%Y#4)+$P("^31^59^90^120^151^181^212^243","^",%M)
Injektion der Werte eine Tuples
std::tuple<int, double, ... , std::string> t(1, 4.0, ..., "kiwi");
in eine Funktion f(...)
f(1, 4.0, ..., "kiwi");
- 500gr Variadische Templates,
- 1 EL Perfect-Forwarding,
- 2 Stück Template Lambdas,
- 1 x Traits
Rühren und Kneten ⟶ Resultat 14-Zeilen am Schluss.
Was kann man mit forwarding-Referenzen T&&
[Template-Parameter T
]
erreichen:
Triangulation::Triangulation(const Vertices& v, const Triangles& t)
: m_vertices(v), m_triangles(t)
{ /* initialisiere Zeugs... */ }
Vertices vertices = {1, 2, 3};
Triangulation tri(vertices,
Triangles{{1,2,3}})
vertices
wird kopiert 👌Triangles{{1,2,3}}
wird auch kopiert obwohl rvalue und könnte gemoved werden. 😩- Lets fix it ...
Triangulation::Triangulation(const Vertices& v, const Triangles& t)
: m_vertices(v), m_triangles(t)
{ /* initialisiere Zeugs... */ }
Triangulation::Triangulation(const Vertices& v, Triangles&& t)
: m_vertices(v), m_triangles(std::move(t))
{ /* code duplikation + std::move(...) */ }
Vertices vertices = {1,2,3};
Triangulation tri(vertices,
Triangles{{1,2,3}})
vertices
wird kopiert 👌Triangles{{1,2,3}}
wird gemoved. 👌- Code Duplikation 💩 ⟶ Kombinationshölle 😨
forwarding-Referenzen verwenden welche die Value-Kategorie erhalten:
template<typename V, typename T>
Triangulation::Triangulation(V&& v, T&& t)
: m_vertices(std::forward<V>(v))
, m_triangles(std::forward<T>(t))
{ /* initialisiere Zeugs... */ }
Vertices vertices = {1,2,3};
Triangulation tri(vertices,
Triangles{{1,2,3}})
-
vertices
ist lvalue → Compiler deduziertV:= Vertices&
→std::forward
retourniertVertices&
(lvalue) → Kopie. 👌 -
Triangles{{1,2,3}}
ist rvalue → Compiler deduziertT:= Triangles
→std::forward
retourniertTriangles&&
(xvalue) → Move. 👌 - Keine Code-Duplikation. 👌
- Klassen/Funktionen
template<typename Iterable> void findZero(const Iterable& it){ ... }
- Lambdas
[](auto v){ std::cout << v;} // auto <=> typename T
- Typedefinitionen: Alias-Templates:
template<typename T> using Map = std::map<int, T*>; // -> Map<float> a;
Don't: Old-School übertypedef
undstruct
→ unleserlich:template<typename T> struct Trait { typedef std::map<int, T*> type; }; Trait<float>::type; // std::map<int, float*>
- Template Parameter
// vvvv-------- Template Parameter template<typename Type> void print(Type v);
- Template Argument
// vvv--------------------- Template Argument print<int>(3);
std::vector<int>
ist ein Typ.std::vector
ist kein Typ sondern ein Template mit 2 Template-Parameter. Im Beispiel:Type
kann niestd::vector
sein!- Traits sind Klassentemplates mit intern definierten Typen: [type_traits]
template<typename T> struct std::add_pointer{ using type = T*; };
#include <type_traits>
static_assert(std::is_same<DataType, CVertexRef>::value, "Wups!");
static_assert(std::is_same<DataType, CVertexRef>{}, "Wups!");
- C++14 : Variable Template
static_assert(std::is_same_v<DataType, CVertexRef>, "Wups!");
template<typename T, typename U>
constexpr bool is_same_v = is_same<T, U>::value;
- Type Template-Parameter
template<typename T> struct A{}; // `class T` ⟶ das selbe (dont!)
- Non-Type Template-Parameter
Don't. Template Argument-Matching schwierig bis unmöglich!
template<std::size_t N> struct A{}; template<MyEnum EMode> struct B{}; template<auto N> struct C{}; // C++20
Wrappe alles in Typen, z.B.:
std::integral_constant<int, 3>
mittypename Number
- Template-Template-Parameter
Don't. Das will man immer vermeiden! Es gibt bessere Konzepte (siehe Alias/Callables [meta]).template<template<typename> class T> struct A{};
Variadische Parameter [Live]
template<typename... Types>
class Converter{// ^^^^^-------------------- Parameter-Pack
public:
using Tuple = std::tuple<Types...>;
// ^^^--------- Pack-Expansion
private:
Tuple m_tuple;
std::array<int, sizeof...(Types)> m_count;
// ^^^^^^^^^----------------- Anzahl Parameter
};
Converter<int, float, double> c; // 1. `Tuple` und `m_count`?
-
Tuple := std::tuple<int, float, double>
-
m_count := std::array<int, 3>
Variadische Parameter [Live]
Meta-Programming: Rechnen mit Typen zu Kompilierzeit:
using List = meta::list<double, float, int>;
using ListNew = meta::transform<List,
meta::quote<std::add_pointer_t>>;
ListNew::DJBobo;
error: 'DJBobo' is not a member of
'ListNew' {aka 'meta::list<double*, float*, int*>'}
foo<int&>();
template<typename T>
void foo()
{
const T& temp = 3; // 'T' ist 'int&'
// 'temp' ist 'const (int&) &' ⟶ 😵 😵
}
(T&) &
⟶ kollabiert zuT&
(T&) &&
⟶ kollabiert zuT&
(T&&) &
⟶ kollabiert zuT&
(T&&) &&
⟶ kollabiert zuT&&
Eselsbrücke:
Einfache Referenz &
gewinnt immer.
template<typename T>
void foo(T& v);
int main()
{
int&& a = 3;
foo<int&&>(a); // 1. Typ von `v`.
}
T& := (int&&) & := int&
Template Argument Deduction [temp.deduct.call]
Banane c;
add(c);
// ^----------------- Argument Typ ⟶ trafo ⟶ definiert Typ `A`
template<typename T>
void add(const T& val);
// ^^^^^^^^----- Parameter Typ ⟶ trafo ⟶ definiert Typ `P`
Template Parameter T
wird vom Compiler automatisch deduziert.
Der Kompiler arbeitet mit 2 Typen A
und P
.
A := Banane
P := T
Matche A
mit P
:
⟶ Resultat: T := Banane
Was sind die Regeln und wie wird gematched?
Template Argument Deduction [temp.deduct.call]
Es gibt genau drei Unterscheidungs-Fälle:
- Deklaration
T
:template<typename T> void add(T val); // T <=> auto
- Deklaration
T&
(lvalue-Referenz):template<typename T> void add( T& val); // T& <=> auto& template<typename T> void add(const T& val); // T& <=> const auto&
- Deklaration
T&&
(forwarding-Referenz):template<typename T> void add(T&& val); // T&& <=> auto&&
Deduktion bei T
oder auto
[temp.deduct.call]
const Banane& c = ...;
add(c);
// ^-------------- [entferne const, etc...] -> A := Banane
template<typename T>
void add(T val);
// ^---------------------------------- -> P := T
- Argument-Typ
A
:
Expr.c
:const Banane
[7.2.2#1]
Trafos:- Entferne
const
vonA
. [13.9.2.1#2.3] - und noch andere unwichtige Traforegeln auf
A
.
- Entferne
- Parameter-Typ
P
:
Trafos: keine.P
wird zuT
. [13.9.2.1#2 impli.] - Pattern-Match:
P
mitA
ergibtT := Banane
T
wird nie automatisch zu einer Referenz. add<int&>(3)
ist keine automatische Deduktion!
Deduktion bei T&
oder auto&
[temp.deduct.call]
const Banane& c = ...;
add(c);
// ^--------------------------------------- -> A := const Banane
template<typename T>
void add(T& val); // (oder const T&)
// ^^--------- [entferne const und &] -> P := T
- Argument-Typ
A
:
Expr.c
:const Banane
[7.2.2#1]
Trafos: keine. (const
darf z.B. nicht entfernt werden!) - Parameter-Typ
P
:
Trafos: Entferneconst
und Referenz&
[13.9.2.1#3]
P
mit A
. (ergibt T := const Banane
)Deduktion bei T&&
oder auto&&
[temp.deduct.call]
T&&
[Template-ParameterT
]auto&&
sind forwarding-Referenzen.
Bei der automatischen Deduktion können diese zu einer lvalue-Referenz (&) oder einer rvalue-Referenz (&&) werden!
Deduktion bei T&&
oder auto&&
[temp.deduct.call]
const Banane& c;
add(c);
// ^-------------- [lvalue -> füge & hinzu] -> A := const Banane&
add(4);
// ^-------------- [rvalue -> nichts] -> A := int
template<typename T>
void add(T&& val);
// ^^^------- [entferne const und &] -> P := T
- Argument-Typ
A
:
Trafos: [13.9.2.1#3]- Expr.
c
ist [const Banane
, lvalue] ⟶ Add&
⟶A := const Banane&
- Expr.
4
ist [int
, prvalue] ⟶A := int
- Expr.
- Parameter-Typ
P
:
Trafos: Siehe Deklaration T&.P := T
. - Pattern-Match:
T := const Banane&
,T := int
⟶ Ref. Collapse!
Deklaration T&&
[temp.deduct.call]
const Banane& a;
add(a);
// ^------------- 1. -> val := (const Banane&) && := const Banane&
add(4);
// ^------------- 2. -> val := (int) && := int&&
template<typename T>
void add(T&& val);
- lvalue wird weitergegeben als lvalue-Reference ⟶ 👌
- rvalue wird weitergegeben als rvalue-Reference ⟶ 👌
Deklaration T&&
[temp.deduct.call]
template<typename T>
void add(T&& val)
{
add(val); // Ungut: `val` wird immer als lvalue weitergegeben
}
val
ist lvalue und zweideutiger Typ: rvalue/lvalue-Reference.
T&&
zu erhalten, brauchts immer
std::forward<T>
:
template<typename T>
void add(T&& val)
{
add(std::forward<T>(val)); // Richtig!
}
Quiz [Live]
Was ist der Typ von t
?
Banane&& a = 4;
foo(a);
// ^--------------------------- A := ???
template<typename T>
void foo(T&& t);
// ??? ------------------- P := ???
- Für
A
: Expressiona
ist [Banane
, lvalue] ⟶ add&
⟶A := Banane&
- Für
P
: ⟶ entferneconst/&
⟶P := T
. - Pattern-Match: ⟶
T := Banane&
⟶ Ref. Coll. ⟶T&& := Banane&
Zwei Overloads:
- Um lvalues als lvalues/rvalues zu forwarden (abh. von
T
)
template<typename T>
T&& forward(lvalue-Reference v){
return static_cast<T&&>(v);
};
- Um rvalues nur als rvalues zu forwarden (Esotherische Cases)
template<typename T>
T&& forward(rvalue-Reference v){
// static-assert: T keine lvalue-Reference
return static_cast<T&&>(v);
};
Merke: std::forward
geht immer zusammen mit einer forwarding-Referenz T&&
!
template<typename T>;
struct Shake {
void doIt(T&& fruit);
// ^^^ -------- keine forwarding-Referenz,
// da `T` hier nicht mehr deduziert wird!
}
template<typename T>
struct Shake {
template<typename F>
void doIt(F&& fruit);
// ^^^ -------- forwarding-Referenz, da deduziert!
// ('const F&&' wäre keine)
}
std::move(expr)
wandelt die Expressionexpr
zu einer rvalue um (retourniert ein xvalue). Man macht diesen Cast um anzugeben, dass man sich nicht länger um den Wert vonexpr
kümmert nach der Auswertung der Expression in welchem dieser Wert gebraucht wurde. Zum Beispiel:
y = std::move(x);
// `y` hat den Wert von `x` und `x` interessiert uns nicht mehr
x = getNewValue(); // weil wir (optional) einen neuen Wert zuweisen.
Merke:
Wann immer man eine rvalue-Referenz hat (e.g. Banane&& rB
, d.h. es geht um temporäre Objekte), kommt std::move
zur Anwendung.
std::forward<T>(expr)
is ähnlich zustd::move(expr)
und kann zu einem rvalue umwandeln. Es hat jedoch zwei Eingaben:expr
undT
.T
wird benutzt um zu entscheiden ob ein lvalue oder ein rvalue retourniert wird. WennT
eine lvalue-Referenz ist, dann wird eine lvalue-Referenz (&) aufexpr
retourniert ansonsten eine rvalue-Referenz (&&).
struct A{} x,y,z;
y = std::forward<A&>(x); // `x` wird nach `y` kopiert
z = std::forward<A>(x); // `x` wird nach `z` gemoved.
Merke: Wann immer man eine forwarding-Referenz hat:
template<typename T> foo(T&& rB)
kommt std::forward<T>(rB)
zur Anwendung.
Kiwi k;
auto spShake = std::make_unique<Shake>(Banane{"mushy"}, k);
template<typename T, typename... Args>
auto make_unique(Args&&... args)
{
return std::unique_ptr<T>(new T(std::forward<Args>(args)...));
}
args
werden per Perfect-Forwarding
an den Konstruktor von T
übergeben:
Banane{"mushy"}
wird gemoved.Kiwi k
wird kopiert.
struct Functor{ void operator()(int a, int b){/*...*/} };
Functor func;
template<typename F>
void apply(int a, F&& f)
{
f(a, 10);
}
apply(5, func); // 1. `F&&` -> `Functor&`
apply(5, createFunctor()) // 2. `F&&` -> `Functor&&`
apply(5, [](int& a, int b){ a += b; }); // 3. `F&&` -> `XXXX&&`
- forwarding-Reference: Vermeidet Codeduplikationen und erweitert die Anwendbarkeit!
- Benutze
static_assert(...)
für gewisse Typen-Checks.
struct Banane
{
template<typename T>
Banane(T&& value);
Banane(const Banane& rBanane); // wird verdeckt!
};
Wird Probleme geben, da T&&
so möglichst alles matched was man sich vorstellen kann:
- Der Copy-CTOR wird nie matchen können, weil
T&&
stärker! T&&
irgendwie entfernen und Overloading verwenden oder falls alles nicht hilft- SFINAE verwenden (später)
Wir möchten folgendes:
auto tuple = std::make_tuple(1, 2.0, "Banane");
invoke(tuple,
[](int a, double b, const std::string& c)
{
std::cout << a << ", " << b << ", " << c << std::endl;
});
Output:
1, 2.0, Banane
Wir möchten folgendes:
invoke(std::make_tuple(1, 2.0, "Banane"),
[](int a, double b, const std::string& c){});
Wie erreichen wir das:
- Template Funktion
invoke
. - forwarding-Referenzen brauchen, damit
invoke(std::make_tuple(...))
möglich. - Meta-Programming möglichst einfach und lesbar! ⟶ Template Lambdas und Variadische Parameter
Args...
wegen Tuple. std::get<I>(tuple)
um denI
-ten Wert des Tuples zurückzugeben.
Expandiere Tuple in Funktion
C++17/20, [Live]
template<typename T, typename F>
void invoke(T&& tuple, F&& func)
{
auto makeRange = []<typename... Args> (std::tuple<Args...>)
{ // std::index_sequence<0,1,2,3,..., N-1>
return std::make_index_sequence<sizeof...(Args)>{};
};
auto call = [&]<std::size_t... I> (std::index_sequence<I...>){
func(std::get<I>(tuple)...);
};
call(makeRange(tuple));
}
- C++17: Lambdas sind implizit
constexpr
falls möglich.
Maybe Teil 3: Spezialisierung, Sfinae, etc... ?