C2cpp Notes: Iterators, Metaprogramming, Smart Pointers
Published:
11. Iterators
Template territory
- Generalized concept of a pointer
Examples
Array Iterator
template <typename I, typename F>
void For_Each(I b, I e, F f) {
while (b != e) {
f(*b);
++b;
}
}
void print_int(int x) { cout << x << ' '; }
int a[5] = { 100, 101, 102, 103, 104 };
For_Each(a + 1, a + 5, &print_int);
I: type pointer (E.int*)- Half interval
[b, e) b: first elemente: one past last element (b+ size)
- Half interval
F: function pointer (E.void (*) int)
Vector Iterator
Vector guarantees contiguous memory, same thing
vector<int> v { 100, 101, 102, 103, 104 };
For_Each(&v[0], &v[0] + 5, &print_int); // raw pointers
For_Each(v.begin(), v.end(), &print_int); // iterators
Don’t forget
&v.begin(),v.end()return iterators of typestd::vector<int>::iterator!- Behave like
int* - Defined inside
vectorclass operator*()defined to callf(*b)operator++()defined- Pointing one past the last element is fine
- As long as you don’t dereference it
- Behave like
F may not be a function, but a class that defines function operator
- Function object
List Iterator
Also works same way with list!
list<int> v { 100, 101, 102, 103, 104 };
For_Each(v.begin(), v.end(), &print_int);
Can’t be implemented with a raw pointer
template <typename T>
class list {
class iterator {
operator!=();
operator*();
iterator& operator++(); // ++i
// ...
};
};
- Not all iterators are equal. They have different categories, with different methods
list<T>::iteratordoes not defineop<=(), useop!=()instead
Why
++b, notb++?
More efficient return value
CPP defines:
// ++b prefix
iterator& operator++() {
curr = curr -> next;
return *this;
}
// b++ postfix
iterator operator++(int unused) {
iterator prev(*this) // need CC previous iteator
curr = curr -> next;
return prev;
}
Range-Based for Loop
Declare iterator list<int>::iterator
- Nested class under
list - Dereferencing gives
int&for (list<int>::iterator i = v.begin(); i != v.end(); ++i) { *i += 200; }
Declare const iterator list<int>::const_iterator
- Different nested class
- Can’t change the underlying element
- Dereferencing gives
const int&
auto: Let compiler figure out type
for (auto i = v.begin(); i != v.end(); ++i) {
cout << *i << ' ';
}
Can also do range-based for loop
- Translated to
auto& x = *i; - Only works if container
vdefines abegin()andend()
for (auto& x : v) {
cout << x << ' ';
}
Vector Initialization
Default Initialization
template <typename SrcIter, typename DestIter>
DestIter copy(SrcIter b_src, SrcIter e_src, DestIter b_dest) {
while (b_src != e_src) {
*b_dest = *b_src;
++b_src;
++b_dest;
}
return b_dest;
}
- Assume destination has sufficient room, and overwrites each element
vector<int> v1 { 100, 101, 102, 103, 104 };
vector<int> v2; // uninitialized
copy(v1.begin(), v1.end(), v2.begin());
Crashes
v2not initialized. Dereferencingv2.begin()crashes.
Need initializer to default construct 5 elements:
vector<int> v2(5);
Initializer List
Curly braces: vector(std::initializer_list<T>)
- Compiler will construct a temporary object of
initializer_list, a pointer to begin and end of actual elements- Elements are constructed in RO memory
- Has
begin()andend()(container)
- On construction, an
initializer_listis passed, and constructsv1vectoronly has constructor that takes aninitializer_list
- Elements may not be constant:
{ 1, f() }- Compiler will construct array, construct initializer, and manage its lifetime Parentheses:
vector(size_t)
- Compiler will construct array, construct initializer, and manage its lifetime Parentheses:
- Initializes vector with default constructed elements
Iterator Adapters
Back Inserter
Fancy way to implement the iterator
- Works with uninitialized vectors by decoupling the operations
- Simplified version of
std::back_insert_iterator
Class template with Container
- Pass
<vector<int>>
template <typename Container>
struct IteratorThatPushes {
Container& c;
// noop dummdies
explicit IteratorThatPushes(Container& c) : c(c) {}
IteratorThatPushes& operator*() { return *this; }
IteratorThatPushes& operator++() { return *this; }
IteratorThatPushes operator++(int) { return *this; }
IteratorThatPushes& operator=(const typename Container::value_type& other) {
c.push_back(other);
return *this;
}
};
explicit: Prevents implicit construction promotion
Hold a reference to container, instead of elements *b_dest = *b_src;
opeator*(): trick to not actually dereference, but return the iterator itself- Triggers the
opeartor=()on the iterator, not the element*b_srchas typeContainer::value_type&- Nested type of every container
- For nested type, need prefix
typename - Or it may not know
value_typeis a type, or a class variable++b_desthas no effect.
Usage
vector<int> v1 { 100, 101, 102, 103, 104 };
vector<int> v2; // uninitialized!
copy(v1.begin(), v1.end(), IteratorThatPushes<vector<int>>{v2});
In STL:
copy(v1.begin(), v1.end(), back_inserter(v2));
ostream_iterator
Adapter that wraps an ostream
- Write by copying elements into
ostream_iterator - Re-casting of
opeator<<()
ostream_iterator<int> oi {cout, /*delim*/ "\n"};
copy(v.begin(), v.end(), oi); // prints
isteam_iterator
vec<int> v;
istream_iteartor<int> iib {cin};
istream_iterator<int> iie {}; // need an end for copy()
copy(iib, iie, back_inserter(v));
- Construction: performs
op>> op++()performs anotherop>>op*()returns the last read valueop!=()compares internal state pointers- On EOF, or default construction, internal set to
nullptr
- On EOF, or default construction, internal set to
Iterator Categories
Not actual types
Renamed into iterator concepts in C++20, all old becomes “Legacy”
LegacyOutputIterator: writing through single-pass increment- “Single-pass”: once read it, can’t go back again
++might invalidate copies!
- Alternating assignment and increment
op=(end)only!- E.
back_insert_iteratorandostream_iteratorLeagacyInputIterator: reading through single-pass increment
- Read exactly once
- E.
istream_iteratorLegacyForwardIterator: Supports input iterator and multi-pass increment
- Iterator can only be incremented, but range can be traversed with multiple iterators
op=()anywhere- E.
std::forward_list::iteratorcan only go forwardLegacyBidirectionalIterator: + decrement
- E.
std::list::iteratorLegacyRandomAccessIterator: + $O(1)$ element access
op<()allowed- E.
std::deque::iteratorLegacyContiguousIterator: + Contiguous elements in memory
std::vector::iterator,std::array::iterator
- “Single-pass”: once read it, can’t go back again
12. Associative Containers
Data stored based on a key, rather than index
Map
std::mapandstd::unordered_map
Forop[], if no entries exist, will default construct one!
- Not a
constoperation, though
std::map is a sorted, associative Container of <key,value> pairs
- Dictionary in Python
- Unique key (associative)
- Sorted: need
op<for keys- Implemented with binary (red black) tree in cpp
- No other pointers, references, iterators are invalidated in insertion/removal
- How to traverse? Using parent, left, right pointers
template <
class Key,
class T,
class Compare = std::less<Key>,
class Allocator //... // special allocation, instead of new
> class map;
std::lessis a functor, withop()
op[]
Atomic find-insert
map<string,int> m; // declare empty map
for (string s; cin >> s; ) {
++m[s];
}
- Read a string from
cin - Increments key
s - If key is not there,
operator[]will put a default constructed value (0)- aka value initializing
- Return
T&
find()
std::map has a find() const function for keys
- Returns an iterator
m.end()if not found
auto it = m.find("string");
if (it != m.end()) {
auto& [s, n] = *it;
}
Iterator
Print output:
for (auto i = m.begin(); i != m.end(); ++i) {
cout << setw(20) << (*i).first << '|';
cout << i->second << '\n';
}
- Printed in alphabetical order
- In-order traversal of binary tree with iterator
i
- In-order traversal of binary tree with iterator
- Deref
*ito get pairi->firstandi->second- Iterators have
operator->defined- If LHS of
operator->is not a raw pointer , keep applying-> - Will uncover
i
- If LHS of
or do:
for (auto& e : m)
// e.first...
for (auto& [s, n] : m) // structured binding
cout << s << '|' << n << '\n';
Unordered Map
std::unorderedmap
unordered_map<string,int> m;
Stored with hash table
- Lookup is usually \(O(1)\) Requirements
- Key must be equality comparable
- Regular map requires
<
- Regular map requires
- Key hashing
template<
class Key,
class T,
class Hash = std::hash<Key>,
class KeyEqual = std::equal_to<Key>,
class allocator
> class unordered_map;
template< class Key >
struct hash;
- No reference/pointers invalidated
- Iterators can be invalidated, if insertion causes rehashing
- Iterator needs to contain information of current bucket
- Hash table is implemented with a single singly linked list
- Bucket points to different places in the list
- Point to one before the element, due to singly linking
- Messed up at re-bucket
Ordered Multimap
std::multimap is a map with multiple entries of the same key
- Sorted by key
unordered_map<string, int> word_to_freq; // construct it...
multimap<int,string> freq_to_words;
for (const auto& [word, freq] : word_to_freq) {
freq_to_words.insert( {freq, word} ); // reverse pair
}
.inserttakes astd::pair<int,string> {freq, word}pair<>omitted for simple structures. Will directly invoke thepairconstruction.
To find, can’t directly use iterator find():
auto [b, e] = freq_to_words.equal_range(3);
if (b != e) {
auto& freq = b->first;
auto num_words = distance(b, e);
}
equal_range(key): return a pair of iterators:- Iterator to first element; to one past last element
std::distance()gives distance between two iterators- Implemented by increment walk
- Can do
e - bif continuous. How do you know?
13. Template Metaprogramming
Computations and validations at compile time!
Intro
MyDistance () takes two iterators
template <typename I>
size_t MyDistance(I b, I e)
return e - b;
- Only works if
op-()works (random access)
int main() {
vector v { 100, 101, 102, 103, 104 }; // OK
forward_list v { 100, 101, 102, 103, 104 }; // compiler error
auto b = ranges::begin(v);
auto e = ranges::end(v);
cout << "Number of elements: " << MyDistance(b, e) << '\n';
}
ranges::begin(v)instead ofv.begin(), to be compatible with rawint[]- Function template. Specialization for raw types
Want to tell at compile time, decide what I is, and invoke different codes
Tag Dispatching
C++ defines empty structs: iterator tags:
struct input_iterator_tag {};
struct output_iterator_tag {};
struct forward_iterator_tag : public input_iterator_tag {};
struct bidirectional_iterator_tag : public forward_iterator_tag {};
struct random_access_iterator_tag : public bidirectional_iterator_tag {};
struct contiguous_iterator_tag : public random_access_iterator_tag {};
Each STL iterator has an iterator_category type inside (typdef)
template <typename T>
struct vector {
struct iterator {
// ...
using iterator_category = forward_iterator_tag;
// ...
};
};
Allows choosing implementations (tag dispatching)
- Passing tag
vector<int>::iterator::iterator_category{}to match the right version Ibecomesvector<int>::iterator
template <typename I>
size_t __doMyDistance(I b, I e, random_access_iterator_tag) {
return e - b;
}
template <typename I>
size_t __doMyDistance(I b, I e, forward_iterator_tag) {
size_t n = 0;
while (b != e) {
++n;
++b;
}
return n;
}
template <typename I> // vector<>::iterator
size_t MyDistance(I b, I e) {
// for containers
return __doMyDistance(b, e, typename I::iterator_category{});
// for raw pointers
return __doMyDistance(b, e, typename iterator_traits<I>::iterator_category{});
}
Raw Pointer
int* doesn’t have ::iterator_category!
- Use
iterator_traits<int*>, whoseiterator_categoryresolves to the tag.
template <typename I>
struct iterator_traits {
using iterator_category = typename I::iterator_category;
// ...
};
// Specialization for raw pointer, speical type T
template <typename T>
struct iterator_traits<T*> {
using iterator_category = contiguous_iterator_tag;
// ...
};
- For iterators, simply return its
::iterator_category - For raw pointers, assign it to
contiguousCompiler will match the second, more specific version with<T*>(template specialization)
SFINAE
Substitution Failure Is Not An Error
std::enable_if
Class template with (value, type)
template <bool B, typename T>
struct enable_if {};
// Specialization for `true` value parameter
template <typename T>
struct enable_if<true, T> { using type = T; };
Specialized so if true, defined with a nested type that resolves to T
enable_if<true, size_t>::type x = 5; // Same as: size_t x = 5;
enable_if<false,size_t>::type y = 5; // Compiler error
std::is_same_v
template <typename T, typename U>
struct is_same {
static constexpr bool value = false;
};
// Specialization for T == U
template <typename T>
struct is_same<T, T> {
static constexpr bool value = true;
};
static: per class, not per instance- Referred to as
Class::value
- Referred to as
constexprtells compiler to evaluate at compile time
template <typename T, typename U>
constexpr bool is_same_v = is_same<T, U>::value;
- STL uses
std::is_same_vto access the::value- Variable template (vs function temp, class temp)
MyDistance() Implementation
Fancy way to write the return type of MyDistance() (size_t)
- If iterator type match, will be
size_t - If mismatch,
enable_if<>::typewill be ill-formed- Compiler error, but compiler will look for other matches
- SFINAE!
template <typename I>
std::enable_if<
is_same_v<
random_access_iterator_tag, // not contiguous tag due to legacy
typename iterator_traits<I>::iterator_category
>,
size_t
>::type MyDistance(I b, I e) {
return e - b;
}
template <typename I>
std::enable_if<
is_same_v<
forward_iterator_tag,
typename iterator_traits<I>::iterator_category>,
size_t>::type
MyDistance(I b, I e) {
size_t n = 0;
while (b != e) {
++n;
++b;
}
return n;
}
Fail if the container is
list
We’ve only checked exact iterator tag matches
if constexpr
- Only choose one branch to compile
ifnot evaluated at runtime!
template <typename I>
size_t MyDistance(I b, I e) {
if constexpr (is_base_of_v<random_access_iterator_tag,
typename iterator_traits<I>::iterator_category>) {
return e - b;
} else {
size_t n = 0;
while (b != e) {
++n;
++b;
}
return n;
}
}
- If
random_access_iterator_tagis a base class ofiterator_traits<I>::iterator_category- Similar to
is_same_v
- Similar to
- More readable version of
If we pass nonsense expressions MyDistance("AAA"s, "BBB"s);
- Won’t compile, because no expression
iterator_traits<string>::iterator_category - Can we limit
Ito be iterator? Concepts!
Concepts
Explicitly states type requirement
- Compile time, evaluate predicate
requiresIto be a forward iteratorforward_iteratoris a concept- Enforce
Ito satisfy the constraints offorward_iterator- can be deref, increment, has iterator tag derived from
forward_iterator_tag
- can be deref, increment, has iterator tag derived from
template <typename I>
requires {forward_iterator<I>}
size_t MyDistance(I b, I e) { ... }
Or, merge with typename
template <forward_iterator I>
size_t MyDistance(I b, I e) { ... }
Concepts to constrain auto
random_access_iterator auto b = ranges::begin(v);
If not ra_iterator, won’t compile!
- E.
vis a list
Define concepts
Compile-time Boolean predicates
template <typename I>
concept MyBidirectionalIterator =
forward_iterator<I> &&
derived_from<
typename iterator_traits<I>::iterator_category,
bidirectional_iterator_tag
> &&
requires (I i) {
{ --i } -> std::same_as<I&>;
{ i-- } -> std::same_as<I>;
};
- Enforce
forward_iterator<I> - Additional functionality: check the iterator category of
Iis derived frombidir_iter_tagderived_fromis also a concept
requires: tries to compile with dummyi- Check if expressions
{i--}compiles and has the right return type - Further require the type using
std::same_as<>- Also a concept
- Compiled as
std::same_as<decltype(--i), I&>
- Check if expressions
14. Functional Programming
Function Object
aka Functor
void print_int(int x) {cout << x << ' '; }
struct IntPrinter {
void operator()(int x) const { cout << x << ' '; }
};
A struct (class), that defines operator()()!
- No data, just a method
For_Each(v2.begin(), v2.end(), &print_int); cout << '\n';
For_Each(v2.begin(), v2.end(), IntPrinter{}); cout << '\n';
- Can pass pointer to function
- Or object (instance of)
IntPrinter{}, default constructed- Callable object! Call to
f(*b)invokesop()of the object
- Callable object! Call to
- A functor can store state information
Stateless Functors
For_Each(v2.begin(), v2.end(), negate<int>{});
For_Each(v2.begin(), v2.end(), IntPrinter{}); cout << '\n';
std::negate can be defined as:
constexpr T operator()(const T& arg) const {
return -arg;
}
No effect, since negate doesn’t assign back to arg
Need to define transform()
template <typename SrcIter, typename DestIter, typename F>
DestIter Transform(SrcIter b_src, SrcIter e_src, DestIter b_dest, F f) {
while (b_src != e_src) {
*b_dest = f(*b_src);
++b_dest;
++b_src;
}
return b_dest;
}
transform(v1.begin(), v1.end(), back_inserter(v2), negate<int>{});
For_Each(v2.begin(), v2.end(), IntPrinter{}); cout << '\n';
- Same as
copy(), using iteratorback_inserter(v2)- Turn assignment
*b_dest = ...into.push_back()call
- Turn assignment
Stateful Functors
struct AddX {
int x;
AddX(int x) : x(x) {}
int operator()(int y) const { return x + y; }
};
When operator() called, adds x
Can also let functor to carry around a parameterized function to call
template <typename F>
struct OpX {
F op; // operator
int x; // number
OpX(F op, int x) : op(op), x(x) {}
int operator()(int y) const { return op(x, y); }
};
transform(v1.begin(), v1.end(), back_inserter(v2),
OpX{plus<int>{}, 300});
- For construction, pass operator and number:
OpX{ plus<int>{}, 300 }OpXis parameterized by any invocable typeF
std::plus<int>is also a functor (binary)OpXbinds first argument tox, yielding a unary functorplus(300, y)that can be called bytransform()with one argument!
template <typename T>
constexpr T operator() (const T& lhs, const T& rhs) const {
return lhs + rhs;
}
Bind Expressions
bind_front
STL provides function template bind_front() that can take a partial list of argument
- More efficient that
std::bind()
void f(arg1, arg2, arg3);
auto g = bind_front(f, arg1, arg2);
// same as:
g(arg3);
transform(v1.begin(), v1.end(), back_inserter(v2),
bind_front(plus<int>{}, 300));
Also bind_back
bind
Use placeholders to specify unbound arguments
- Clumsy, not used much
struct SubSubSub {
int operator()(int a, int b, int c, int d) const
return a - b - c - d;
};
auto f = bind(
SubSubSub{}, // default construction
1000, // a: 1000
placeholders::_2, // b: second new argument
placeholders::_1, // c: first new argument
placeholders::_1 // d: first new argument again
);
f(100, 500); // same as SubSubSub(1000 - 500 - 100 - 100)
bind()always take the same arguments (4)
transform(v1.begin(), v1.end(), back_inserter(v2),
bind(plus<int>{}, 300, placeholders::_1));
Function Composition Binding
Let f(x) = 300 + g(x), g(x) = -x
transform(v1.begin(), v1.end(), back_inserter(v2),
bind(
plus<int>{},
300,
bind(negate<int>{}, placeholders::_1) // g(x) = -arg1
)
);
Same as:
auto g = bind(negate<int>{}, placeholders::1);
auto f = bind(plus<int>{}, 300, g); // plus<int> is a type; g is an instance
transform(..., f);
Lambda
Return unnamed functors
transform(v1.begin(), v1.end(), back_inserter(v2),
[] (int x) {
return 300 + -x;
}
);
- Start with
[]: lambda introducer - Rest of it looks like function
- No need to deduce return type!
Closures
Lambda provides closure, a function that captures the local environment
vector<int> v1 { 100, 101, 102, 103, 104 };
vector<int> v2;
// local variables
int a = 2, b = 100;
auto lambda = [&a, b] (int x) {
return a * x + b;
};
a = 4, b = 1'000'000; // update local variable, b to 1 million unaffected!
transform(v1.begin(), v1.end(), back_inserter(v2), lambda);
// return 4 * x + 100
- Variable
lambdaholding the function [&a, b]allowslambdato access the local variables&acapturesaas a referencebcopiesbby value
- If
lambdaoutlives the captured variable, undefined behavior!!
What class is
lambda?
Withauto, compiler will make a unnanmed temporary class with an internal name
Lambda Internals
struct L1 {
int& a;
int b;
L1(int&a, int b) : a(a), b(b) {}
int operator()(int x) const { return a * x + b; } // const member function!
};
int a = 2, b = 100;
auto lambda = L1{a, b}; // same type as L1
a = 4, b = 1000000;
To make operator()notconst, use [[#lambdasmutable]]
Ranges
Concept generalization of a sequence of elements in STL denoted by half-open interval
std::ranges::range is a concept that has begin() and end()
- E. any container
- E. C array
template <typename T> concept range = requires(T& t) { ranges::begin(t); ranges::end(t); }; begin()is an iteratorend()is a sentinel (generalization of iterator)- Just need an object that implements
op==to compare if iterator has reached the end. - E. Infinite range:
op==always returnsfalse
- Just need an object that implements
Views
Counterpart of a container; subset of
concept range
Lightweight, read-only range that doesn’t own the elements, but wraps another range
- Has a rule to apply for each element (E. transform)
- Also a range, but typically holds a transformation object
Created as transform_view{ range, functor } object
auto v1_view = ranges::transform_view{
ranges::transform_view{ v1, negate<int>{} }, // range
bind_front(plus<int>{}, 300) // functor
};
- Underlying range:
v1vector- required to be a
rangeconcept
- required to be a
- Transformation function: functor
negate<int>{}- If read from this view, will always apply the function
- Returns another range!
ranges::copy(v1_view, back_inserter(v2));
ranges::for_each(v2, IntPrinter{});
- Copy the view to an std vector
v2- Different from
std::copy(begin, end, dst) ranges::copy(range/view, iterator)
- Different from
ranges::for_each(range, functor)
op|: Iterator Adaptor Closure
ana lambda
Similar as Unix piping, connecting transforms
auto v1_view = v1 | views::transform(negate<int>{})
| views::transform(bind_front(plus<int>{}, 300));
views::transform(): function template- Takes a functor (anything callable)
- Returns a range adapter closure
op|(range r, range_adapter_closure C)R | Cequivalent toC(R)- Returns a new range that can be chained
R | views::transform(C)becomesranges::transform_view{R, C}
Append ranges::to<vector<int>>() to create a new vector v3
- Back-insert to the newly-constructed vector and return by value
- Takes a view
v3gets move-constructed
auto v3 = v1 | views::transform(negate<int>{})
| views::transform(bind_front(plus<int>{}, 300))
| ranges::to<vector<int>>();
auto v = views::iota(100)
| views::take(5)
| views::transform(negate<int>{})
| views::transform(bind_front(plus<int>{}, 300))
| views::filter([](int x) { return x % 2 == 0; });
iota(100): creates an infinite range starting from 100take(5): takes first 5 (100, 101, 102, 103, 104)transformsfilter()with a lambda function (returns T/F)- (200, 198, 196)
15. RNG
Native Engine
C
Uniformly generates in the range of [-10, 10]
srandom(time(nullptr));
auto engine = &random; // random() in the C library as engine
auto distro = [] (auto& some_engine) {
return some_engine() % 21 - 10;
};
engine: pointer torandom()functionrandom()returns numbers from 0 to 2^31 - 1
distro: Lambda functor that distribute it from -10 to 10
Usage
map<int,int> m;
for (int i = 0; i < 1'000'000; ++i) {
int x = distro(engine);
++m[x];
}
Cpp
Make
rd,engineanddistroas functors
// seed generation, taking advantage of real-RNG
random_device rd;
random_device::result_type seed;
seed = (rd.entropy() > 0) ? rd() : time(nullptr);
// construct engine and distro
default_random_engine engine { seed };
uniform_int_distribution<> distro { -10, 10 };
int x = distro(engine);
default_random_enginedistrofunctoruniform_int_distribution- Templatized with different integer types
Seed generation
std::random_device: implementation-dependent real RNG- E. in Linux:
/dev/random, generated by IO events (entropy event) rd.entropy()tells ifrdis actually random, or deterministic (=0)
- E. in Linux:
rd()return a seedengine()computes random numberdistro()distributes the number
Mersenne Twister engine
// same setup as generating the seed
mt19937 engine { seed };
normal_distribution<> distro { 0.0, 3.5 };
map<int,int> m;
int maximum = 0;
// round to
int x = lround( distro(engine) );
mt19937: Mersenne Twister (another algorithm)- Smeared through
normal_distributionfloats with \(\mu = 0, \sigma = 3.5\)
Mechanism
C random() has a very large period. Mersenne Twister’s period is much larger!
- Fast
- Almost never repeats
- Very tunable
- High memory requirement
Narrowing Conversion
What if I remove lround():
int x = distro(engine); // silently fails
int x { distro(engine) }; // compiles with warning
- Native conversion throws away the decimal portion!
- Allowed in C, but broke in Cpp
Need static_cast<int>
int x { static_cast<int>(lround(distro(engine))) };
Stateful Functors
Use bind() to compose distro with engine
for (int i = 0; i < 1000000; ++i) {
auto generator = bind(distro, engine);
int x = lround( generator() );
}
Fails to work
Every run generates the same number every time
bind()makes a copy of the functor and its argumentsengine()holds state of current random number- In each
forloop,enginecopied with just the seed!Also runtime is significantly longer, due to copying
mt19937 engine { seed };
cout << "typeid(engine).name():\n" << typeid(engine).name() << '\n';
cout << "\nsizeof(engine): " << sizeof(engine) << '\n';
mt19937is a specialization of templatestd::mersenne_twister_engine<>with tuned parameters- Name internally will create a name with all these template parameters embedded
- Size: 5000 bytes!
In contrast, default_random_engine only has 8 bytes
- Specialization of
std::linear_congruential_engine<>template Only need to carry state of the current number, and 3 parameters
- Can be fixed by defining
generator()out of theforloop, or:
ref()
Pointer wrapper that behaves like a reference
Use std::ref() to pass by reference
auto generator = bind(distro, ref(engine));
Implemented by holding a pointer v_ptr
bind()holds a copy, which is just a pointer- When
bind()actually uses it, behaves similaroperator T&: behavior when automatically converted toT&
template <typename T>
class reference_wrapper {
public:
reference_wrapper(T& v) : v_ptr(&v) {}
operator T&() const { return *v_ptr; }
// ...
private:
T* v_ptr;
};
template <typename T>
reference_wrapper<T> ref(T& v) {
return reference_wrapper<T>(v);
}
Lambdas
auto generator = [distro, engine] () { return distro(engine); };
Will fail, since copying by value
- Fails to compile.
operator() const, butdistro(engine)changes state ofengine!
auto generator = [distro, engine] () mutable { return distro(engine); };
auto generator = [=] () mutable { return distro(engine); };
mutabledoes not enforceconstness ofop()- Compiles, but fails to run due to copying
[=]shorthand for automatically listing[distro, engine]
auto generator = [&distro, &engine] () { return distro(engine); };
auto generator = [&] () { return distro(engine); };
- Works, by reference
- No
mutableneeded, since pointer references not mutated.const T* q: const objectT* const p: stuck pointer (this case)
[&]shorthand for listing[&distro, &engine]
16. Forwarding Reference and Variadic
Recall that these are not allowed
X x;
const X x_c;
X&& x1 = x; // cannot assigned named variable to R-value ref
X&& x2 = x_c;
X& x3 = x{}; // about to die
X& x4 = x_c; // violates const
Value Categories
L-Value and R-Value
Define a wrapper for heap-allocated double
struct D {
D(double x = -1.0) : p{new double(x)} { cout << "(ctor:" << *p << ") "; }
~D() { cout << "(dtor:" << (p ? *p : 0) << ") "; delete p; }
D(const D& d) : p{new double(*d.p)} { cout << "(copy:" << *p << ") "; }
D(D&& d) : p{d.p} { d.p = nullptr; cout << "(move:" << *p << ") "; }
D& operator=(const D& d) = delete;
D& operator=(D&& d) = delete;
// allows conversion to double
operator double&() { return *p; }
operator const double&() const { return *p; }
double* p;
};
D d1 { 1.0 }; // lvalue
D&& rrd4 = D(4.0); // rvalue
rrd4 += 0.1; // rvalue reference is mutable!
D(4.0) is a temporary object on stack, but when they are bound to references, lifetime extended, until references are destroyed!
- After
rrd4goes out of scope, destructor called - If not assigned,
D(4.0)destroyed at end of semicolon
Binding R-Value Reference
D&& rrd4_1 = rrd4;
Fails to compile
rrd4is a reference variable with a name. The referencerrd4itself is an L-value- What it references might be an R-value
- Can’t bind it to R-value referenced
D&& rrd4_1
Need cast rrd4 to an R-value reference by std::move()
D&& rrd4_1 = std::move(rrd4);
std::move()does not move! Now bothrrd4_1andrrd4point to the temp R-value.
Now consider!
D& rrd4_2 = rrd4;
OK
- Bind a temporary object to a L-value reference
D& rrd4_2 - Loophole: normally you can’t assign R-value to L-value!
Summary
- L-value
- Named variable, function call returning by reference
- C string literals
"abc", since they have addresses - R-value itself, since it has a name
- GL-value: L-value + X-value
- R-value: two subcategories
- PR-value (Pure R-value, no address)
- Literals (
2.0), function call returning by value (s + s)
- Literals (
- X-value (eXpiring values, has address, but will die)
- Return/cast to R-value ref (E.
std::move(s))
- Return/cast to R-value ref (E.
- PR-value (Pure R-value, no address)
Forwarding Reference
1. Pass by value
template <typename T> void f1(T t) { t += 0.1; }
{
D d1 {1.0}; // C(1)
f1(d1); // CC(1), D(1.1)
f1(D(2.0)); // C(2), (CC elided), D(2.1)
} // D(1)
Take t as a regular passing by value parameter
- Works with all sorts of values
2. Pass by L-value Reference
template <typename T> void f2(T& t) { t += 0.2; }
{
D d1 {1.0}; // C(1)
f2(d1); // no CC or D!
// f2(D(2.0)); // err: cannot bind rvalue to T&
} // D(1.2)
Take L-value reference
3. Pass by Forwarding Reference
template <typename T> void f3(T&& t) { t += 0.3; }
{
D d1 {1.0}; // C(1)
f3(d1);
f3(D(2.0)); // C(2), D(2.3)
} // D(1.3)
Cpp changed the rule that T&& is no longer a regular R-value reference, but forwarding reference
- Can pass both L-value and R-value
- Only works for function template, not class template!
- R-value reference:
Tresolves toD, soT&&resolves toD&& - L-value reference:
Tresolves toD&, soT&&becomesD& &&, collapsing intoD&
Collapsing Rules
X& & collapses to X&
X& && collapses to X&
X&& & collapses to X&
X&& && collapses to X&&
4. Pass Forwarding Reference to Another Function
Templates
T&&only. Doesn’t work forint&&
Any named reference, whether L-value of R-value, is an L-value!
template <typename T> void f4(T&& t) {
D d{t}; // CC(1); CC(2): further forwarding
d += 0.4;
} // D(1.4); D(2.4)
{
D d1 {1.0}; // C(1)
f4(d1);
f4(D(2.0)); // C(2), D(2)
} // D(1)
All CC called, no MC!
tbecomes named in the function. Will CC
5. Perfect Forwarding
std::forward() preserves the original value category
- Carry R-value forward!
- Shouldn’t use
tanymore in the function!
template <typename T> void f5(T&& t) {
D d{std::forward<T>(t)}; // perfect forwarding, CC becomes MC
d += 0.5;
}
{
D d1 {1.0};
f5(d1);
f5(D(2.0));
}
Now CC for L-values, MC for R-values
6. My Perfect Forwarding
A function that casts input
T&back toT&&
Define some helper class templates:
template<typename T> struct remove_reference { using type = T; };
// specialization
template<typename T> struct remove_reference<T&> { using type = T; };
// changes nested type to be T w/o &
template<typename T> struct remove_reference<T&&> { using type = T; };
template<typename T> using remove_reference_t = typename remove_reference<T>::type;
- E.
remove_reference_t<int&> t = 5same asint t = 5;
Use it:
template<typename T>
T&& forward(remove_reference_t<T>& z) noexcept {
return static_cast<T&&>(z);
}
template<typename T> void f5(T&& t) { D d{ forward<T>(t) }; }
For L-value f5(d1),
T = D&,T&& = D& && = D&- When
forward<T>(t);called, becomesforward<D&>(t)
D& && forward(remove_reference_t<D&>& z) noexcept {
return static_cast<D& &&>(z);
}
which evaluates to:
D& forward(D& z) noexcept {
return static_cast<D&>(z);
}
For R-value f5(d{ 2.0 })
T = D,T&& = D&&forward<D>(t)becomes:
D&& forward(remove_reference_t<D>& z) noexcept {
return static_cast<D&&>(z);
}
No reference to remove, so just D:
D&& forward(D& z) noexcept {
return static_cast<D&&>(z);
}
tisD&&, but also a named L-value, so it’s okay to pass it as L-value inforward(D& z)- Casted back to R-value reference!
Arguments always passed in as
D&, butforward()can recover the return type based on template evaluations
- Question: are these all static?
Variadic Templates
Use
...for more types
typename> type name > variable name Compile time!
Recursion template (3 levels)
- Template parameter pack:
template <typename... Ts> - Function parameter pack:
f(Ts... args) - Packed expansion:
f(args...);(usage)
void print_v1() { cout << '\n'; } // base case for template recursion
template <typename T, typename... Ts>
void print_v1(T arg0, Ts... args) {
cout << arg0 << ' ';
print_v1(args...);
}
Can do anything
print_v1(s, "ABC", 45, string{"xyz"});
// compiler will generate recursively in object file:
void print_v1(string arg0, const char* arg1, int arg2, string arg3) {...}
void print_v1(const char* arg0, int arg1, string arg2) {...}
void print_v1(int arg0, string arg1) {...}
void print_v1(string arg0) {...}
until pack is depleted
Forwarding References
We are currently passing by value
print_v1(s, "ABC", 45, string{"xyz"}, D{3.14});
Passing by value, so at each recursive call (before printing), constructor called (5 times), same for D.
For reference, need to handle L-value and R-value
- Take
MoreTs&&...will apply to such a&&pattern
- Use
std::forward()for perfect forwarding- Same as
print_v2(forward<string>(arg1), forward<string>(arg2), ...)
- Same as
template <typename T, typename... MoreTs>
void print_v2(T&& arg0, MoreTs&&... more_args) {
cout << arg0 << ' ';
print_v2(std::forward<MoreTs>(more_args)...);
}
Just a single C beginning, D end
Fold Expression
Not on exam
...in an expression
(... binary_op pack) // unary left fold
(pack binary_op ...) // unary right fold
(init_expr binary_op ... op pack) // binary left fold
(pack binary_op ... op init_expr) // binary right fold
template <typename... Types>
void print_v3(Types&&... args) {
// Binary left fold without printing spaces
(cout << ... << args) << '\n';
// Unary right fold with comma operator to print spaces
((cout << args << ' '), ...) << '\n';
}
(cout << ... << args) << '\n';: binary left fold
(((cout << arg0) << arg1) << arg2)init_expr:coutbinary_op:<<...op:<<pack:args- Expanded to
(arg0 bin_op (arg1 bin_op (arg2)))
((cout << args << ' '), ...): unary right fold
pack:(cout << args << ' ')binary_op:,(!)...
17. Smart Pointer
[!info] Suicidal Pointer A class with pointer inside and acts like a pointer (
op*(),op->())
- Self-delete when goes out of scope
- How making two copies? Who’s deleting it?
- Shared pointer: know each other; deleted only after all gone
- Unique pointer: Only one can hold it; can only be moved
Shared Pointer
Implementation
template <typename T> class SharedPtr {
T* ptr;
int* count;
public:
explicit SharedPtr(T* p = nullptr): ptr{p}, count{new int{1}} {}
~SharedPtr() {
if(--*count == 0) {
delete count; delete ptr;
}
}
T& operator*() const { return *ptr; }
T* operator->() const { return ptr; }
operator void*() const { return ptr; } // for if (sp)
T* get_prt() const { return ptr; } // get() in STL
int get_count() const { return *count; }
- Constructed with a pointer (E.
new string{}) - Manages heap-allocated data and
count - Last one out of scope is responsible for deletion
SharedPtr*(const SharedPtr<T>& sp): ptr(sp.ptr), count(sp.count) {
++*count;
}
SharedPtr<T>& operator=(const SharedPtr<T>& sp) {
if (this != &sp) { // delete, and then construct
if (--*count == 0) {delete count; delete sp;}
ptr = sp.ptr;
count = sp.count;
++*count;
}
return *this;
}
};
- Copy constructor makes a shallow copy of
ptrandcount; increments*count - Copy assignment, need detach first
Example
SharedPtr<string> p1 = {new string{"hello"}};
auto foo = [=] (SharedPtr<string> p) { // take p by value, CC!
cout << p.get_count() << endl;
}
foo(p1);
This will invoke copy constructor, incrementing count After foo() returns, p goes out of scope, and count decrements back
Vector
Let SharedPtr<string> contain a string* inside
auto create_vec1() {
SharedPtr<string> p1 { new string{"hello"} };
SharedPtr<string> p2 { new string{"c"} };
SharedPtr<string> p3 { new string{"students"} };
vector v { p1, p2, p3 };
return v;
}
vector v1 = create_vec1();
- Shared pointers themselves
p1… are stack-allocatedstringobjects are heap allocated
- When creating a vector, will make another copy of the string!
- Should be
vector<SharedPtr<s
- Should be
- Return by value:
vwill get moved/elided- Stack-allocated
p1goes out of scope,countgoes back to 1When
create_vec1()returns, stack pointers destroyed, andvmoved/elided Now modify it: ```cpp vector v2 = v1; // copy pointers, but still same underlying string *v2[1] = “c2cpp”; *v2[2] = “hackers”;
- Stack-allocated
// print v1 again for (auto&& p : v1) { cout « *p + “ “; } cout « ‘\n’;
1. **Copy construct** `v1` to `v2`
- `count` goes up to 2
2. Modify `v2`: Dereference `SharedPtr` as *raw* pointer and modify it!
3. Print original `v1`: **also modified**!

## Make Shared Pointer
Does the `new` with **any number** of arguments to construct `T`
- [Forwarding Reference](#forwarding-reference)!
```cpp
template <typename T, typename... Args>
SharedPtr<T> MakeSharedPtr(Args&&... args) {
return SharedPtr<T>{ new T{forward<Args>(args)...} };
}
auto p1 { MakeSharedPtr<string>("hello") }; // My implementation
auto p2 { make_shared<string>("c") }; // STL
static_assert(is_same_v< decltype(p2), shared_prt<string> >);
{
string* p0 { new string(50, 'A') }; // leak string and char[]
shared_ptr<string> p1 { new string(50, 'B') };
shared_ptr<string> p2 { new MakeSharedPtr<string>(50, 'C') };
}
MakeSharedPtr has one fewer allocation from Valgrind
- Library notices that
new stringis on the heap, so it combines the control blockcountandstringin a single allocation (placementnew) - Our implementation uses
new string, no such freedom
Deletion
{
SharedPtr<string> p0 { new string[2] {"a", "b"} };
}
Construction fine, but deletion crashes for string[2] ! Need delete[]
Need to write custom functor for delete
SharedPtr<string> p0 {new string[2] {"a", "b"},
[](string* s) {delete[] s;}
}
Can also do for FILEs
shared_ptr<FILE> sp { fp,
[](FILE* fp) {fclose(fp);}
}
Or, directly put <string[]> as the type for STL
for (SharedPtr<string> p : v1) { cout << *p + " "; }
for (auto p : v1)
for (auto& p : v1)
for (const auto& p : v1)
for (auto&& p : v1)
auto&&declares a forwarding referencexcan either be an L-value ref or R-value ref, depending on category of*v1.begin()- Useful if iterator return is unknown
- E.
vector<bool>
