Universal Reference And Perfect Forwarding
Perfect forwarding in C++ preserves both the type and the value category of an input argument.
Refresher - Value categories:
- lvalue
- rvalue
- xvalue (eXpiring)
- prvalue (pure)
Universal Reference
std::forward
calls a callable be it a lvalue, or an rvalue. When a type is deduced in a template, if it’s in the form &&
, it’s a universal reference, Func&&
in this case is the universal reference to func
. So if it’s:
- an lvalue:
profile_and_call(another_function)
,another_function
isanother_function&
, andFunc&&
is deduced toanother_function& &&
and becomesanother_function&
- an rvalue:
profile_and_call(std::move(another_function))
,another_function
isanother_function&&
, andFunc&&
is deduced toanother_function&& &&
and becomesanother_function&&
Specifically, this preserves the value category of the function, specifically when the function inside has move semantics. If it’s a simple const Func& func
argument, we will not be able to move. Also, if the function is a captured lambda, it’s a temporary object that will be best used if it’s “moved”
On the other hand, note that T&& is a universal reference only when T is a deduced template parameter. (so it doesn’t happen outside of the template scenario).
1
void func(int&& arg); // rvalue reference only
The basic format is to declare Func
in template, then forward it: std::forward<TYPE>
1
2
3
4
5
6
7
8
9
10
template<typename Func>
void profile_and_call(Func&& func){
std::forward<Func>(func)(); // perfectly forward and invoke
}
// Using copy ctor:
template<typename Func>
void profile_and_call(const Func& func){
func();
}
- One rule in C++ is “named parameters are lvalues”: inside
void profile_and_call(Func&& func)
,func
itself is an lvalue. we needstd::move()
. orstd::forward
to cast it to the correct value category.
Need Member Template For Class Template
Inside a class template you add a member template so you can perfectly‑forward whatever key/value pair the caller gives you.
1
2
3
4
5
6
7
8
template <typename Key, typename Value>
class HashMap{
...
template <typename K, typename V>
void add(K&& key, V&& value) {
emplace(std::forward<K>(key), std::forward<V>(value));
}
};
- The compiler will have to deduce K, and V and ensures matching with Key and Value
1
itr_lookup_.find(key)
std::move
and Double Moved-From Issue
A “moved-from” state is a valid yet unspecified state. You can assign another value to it, destroy it, etc.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
#include <iostream>
#include <string>
#include <utility>
struct Tracer {
std::string name;
Tracer(std::string n): name(std::move(n)){
std::cout << "[Ctor] name = \"" << name << "\"\n";
}
Tracer(const Tracer& other) : name(other.name) {
std::cout << "[Copy] name = \"" << name << "\"\n";
}
Tracer(Tracer&& other) noexcept : name(std::move(other.name)){
std::cout << "[Move] new.name = \"" << name
<< "\", old.name = \"" << other.name << "\"\n";
}
};
template <typename T>
void double_move(T&& x) {
std::cout << "-> entering double_move, x.name = \"" << x.name << "\"\n";
// First move-from
Tracer a(std::forward<T>(x)); // should see a's move ctor called, x name is valid
std::cout << " after first move, x.name = \"" << x.name << ", a name"<<a.name<<"\"\n";
// Second move-from
Tracer b(std::forward<T>(x)); // should see b's move ctor called, but x's name already is ""
std::cout << " after second move, x.name = \"" << x.name << ", b name"<<b.name<< "\"\n";
}
int main() {
Tracer t("original");
double_move(std::move(t));
std::cout << "-- back in main, t.name = \"" << t.name << "\" --\n";
return 0;
}
- The output shows only move-ctors are called, no copy ctor is called. But the first
std::forward
call has nullified x into a moved-from state, which would be an un-defined state std::move()
is designed to move an lvalue reference into an rvalue reference.double_move(std::move(t));
makest
bind toT&&
, enables perfect forwarding, so move ctor can be called later withstd::forward<TYPE>()
orstd::move<TYPE>()
. Ifdouble_move(t);
is used, everything will be copied.
prvalue
TODO
1
2
auto map_itr = itr_lookup_.find(key); // this is a pr-value
auto& map_itr = itr_lookup_.find(key);
Emplace with std::move
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
#include <iostream>
#include <string>
#include <tuple>
#include <utility>
#include <unordered_map>
//----------------------------------------------------------------------
// A small tracer for keys
struct KeyTracer {
std::string name;
KeyTracer(const std::string& n) : name(n) { std::cout << "[Key Ctor] \"" << name << "\"\n"; }
KeyTracer(const KeyTracer& o) : name(o.name) { std::cout << "[Key Copy] \"" << name << "\"\n"; }
KeyTracer(KeyTracer&& o) noexcept : name(std::move(o.name)) { std::cout << "[Key Move] new=\"" << name << "\" old=\"" << o.name << "\"\n"; }
// equality for unordered_map
friend bool operator==(KeyTracer const& a, KeyTracer const& b) {
return a.name == b.name;
}
};
//----------------------------------------------------------------------
// Provide std::hash<KeyTracer>
namespace std {
template<> struct hash<KeyTracer> {
size_t operator()(KeyTracer const& k) const noexcept {
return std::hash<std::string>()(k.name);
}
};
}
//----------------------------------------------------------------------
// A small tracer for values
struct ValueTracer {
int x, y;
ValueTracer(int _x, int _y) : x(_x), y(_y) {std::cout << "[Value Ctor] (" << x << ", " << y << ")\n"; }
ValueTracer(const ValueTracer& o) : x(o.x), y(o.y) { std::cout << "[Value Copy] (" << x << ", " << y << ")\n"; }
ValueTracer(ValueTracer&& o) noexcept : x(o.x), y(o.y) { std::cout << "[Value Move] new=(" << x << ", " << y << ") old=(" << o.x << ", " << o.y << ")\n";}
};
int main() {
std::unordered_map<KeyTracer,ValueTracer> m;
std::cout << "\n--- emplace with piecewise_construct ---\n";
// construct key and value right on the spot, zero temp, zero move. MOST EFFICIENT
m.emplace(
std::piecewise_construct,
std::forward_as_tuple("apple"), // builds key in-place
std::forward_as_tuple(7, 42) // builds value in-place
); // std::piecewise_construct is needed.
m.emplace(
KeyTracer{"apfel"}, // this constructs a KeyTracer
ValueTracer{7,42} // this constructs a ValueTracer
); // emplace will 1. construct a temp Key and Value object. 2. forwards those temps into std::pair, which moves each once.
// m.emplace({"apfel"}, {7,42}); // Doesn't compile because emplace(Args&&) will foward them.
return 0;
}
- In unordered map,
m.insert(std::pair)
takes in a pair. - As in multiple containers (like std::vector),
m.emplace(Args&&)
takes in arguments to construct elements that can go intom.insert()
orm.push_back()
emplace
:-
std::piecewise_construct
usesstd::pair
’s piecewise ctor1 2 3 4 5
m.emplace( std::piecewise_construct, std::forward_as_tuple("apple"), // → KeyTracer(const char*) std::forward_as_tuple(7, 42) // → ValueTracer(int,int) );
std::forward_as_tuple(arg1, arg2...)
is just a convenient way to create a tuple of universal references to the args.- So
std::forward_as_tuple(7, 42)
goes into aValueTracer(int,int)
ctor
-
Without
std::piecewise_construct
, temps are constructed first, then moved into astd::pair
:1 2 3 4
m.emplace( KeyTracer{"apfel"}, // this constructs a KeyTracer ValueTracer{7,42} // this constructs a ValueTracer ); // emplace will 1. construct a temp Key and Value object. 2. forward and calls move ctor on the temps
-
m.emplaceapfel
fails because:- A function template like
template <class... Args> pair<iterator,bool> emplace(Args&& args);
needs clear typenames, and won’t take in braced-init list {}
- A function template like
try_emplace{Args&&}
is introduced in C++17 that in-place constructs the value only if the key isn’t already present; otherwise does nothing.. It’s idempotent