A Live-Hacked Tour Around The New C++¶
A sketch of something purely nonsense, only there to walk through The New C++
To-Do list: names, and associated items
Items are strings initially
Slowly morphed into things that actually do something (⟶
<functional>
, and lambdas)Ending up in a multithreading massacre
… nicely encapsulated in a class
C++03 To-Do List Version¶
Using
std::map<std::string,std::string>
as clumsy as is the nature of C++03No initialization, only default constructor (⟶ empty), and explicit fill at runtime
Iterators - although I find pointer arithmetic cool, that taste is not shared by many
#include <map>
#include <string>
#include <iostream>
int main()
{
using todo_list = std::map<std::string, std::string>;
todo_list tdl;
tdl["up 1 to 10"] = "prefix: 'UP', count up from 1 to 10, interval 1 second";
tdl["down 1000 to 980"] = "prefix: 'DOWN', count down from 1000 to 980, interval 0.5 second";
for (todo_list::const_iterator it=tdl.begin(); it!= tdl.end(); ++it) {
std::string name = it->first;
std::string desc = it->second;
std::cout << "NAME: " << name << ", DESC: " << desc << std::endl;
}
return 0;
}
Real Container Initialization¶
Think C’s “array of
struct
” initializationstd::map
is initialized ⟶ could beconst
!Almost feels like Python:
dict([(1, "one"), (2, "two")])
⟶
const
#include <map>
#include <string>
#include <iostream>
int main()
{
using todo_list = std::map<std::string, std::string>;
todo_list tdl{ // <--- real initialization: could make it *const* just as well!
{ "up 1 to 10", "prefix: 'UP', count up from 1 to 10, interval 1 second" },
{ "down 1000 to 980", "prefix: 'DOWN', count down from 1000 to 980, interval 0.5 second"},
};
for (todo_list::const_iterator it=tdl.begin(); it!= tdl.end(); ++it) {
std::string name = it->first;
std::string desc = it->second;
std::cout << "NAME: " << name << ", DESC: " << desc << std::endl;
}
return 0;
}
Long iterator
Type Names ⟶ auto
¶
Annoying: long
iterator
namestypedef
is of limited help
#include <map>
#include <string>
#include <iostream>
int main()
{
using todo_list = std::map<std::string, std::string>;
todo_list tdl{
{ "up 1 to 10", "prefix: 'UP', count up from 1 to 10, interval 1 second" },
{ "down 1000 to 980", "prefix: 'DOWN', count down from 1000 to 980, interval 0.5 second"},
};
for (auto it=tdl.begin(); it!= tdl.end(); ++it) { // <---
std::string name = it->first;
std::string desc = it->second;
std::cout << "NAME: " << name << ", DESC: " << desc << std::endl;
}
return 0;
}
More auto
: Unpacking std::pair
¶
More explicit type names
std::pair
‘sfirst
,second
#include <map>
#include <string>
#include <iostream>
int main()
{
using todo_list = std::map<std::string, std::string>;
todo_list tdl{
{ "up 1 to 10", "prefix: 'UP', count up from 1 to 10, interval 1 second" },
{ "down 1000 to 980", "prefix: 'DOWN', count down from 1000 to 980, interval 0.5 second"},
};
for (auto it=tdl.begin(); it!= tdl.end(); ++it) {
auto name = it->first; // <--- compiler knows type std::string anyway
auto desc = it->second; // <---
std::cout << "NAME: " << name << ", DESC: " << desc << std::endl;
}
return 0;
}
Pitfall: Plain auto
Creates Copy ⟶ const auto&
¶
auto
is only the base typeHere: deducing the type of
std::pair
membersOn functions/methods: ignores any
const
, any&
#include <map>
#include <string>
#include <iostream>
int main()
{
using todo_list = std::map<std::string, std::string>;
todo_list tdl{
{ "up 1 to 10", "prefix: 'UP', count up from 1 to 10, interval 1 second" },
{ "down 1000 to 980", "prefix: 'DOWN', count down from 1000 to 980, interval 0.5 second"},
};
for (auto it=tdl.begin(); it!= tdl.end(); ++it) {
const auto& name = it->first; // <--- cheaper than copying std::string
const auto& desc = it->second; // <---
std::cout << "NAME: " << name << ", DESC: " << desc << std::endl;
}
return 0;
}
Iterators Are So Old-School: Range Based For¶
C’s
for
loops are only brutal syntactic sugar forwhile
This can be done better
“I only want to see each element that’s in it!” ⟶ Pythonicity!
Using
auto
all over⟶
const auto&
, obviously
#include <map>
#include <string>
#include <iostream>
int main()
{
using todo_list = std::map<std::string, std::string>;
todo_list tdl{
{ "up 1 to 10", "prefix: 'UP', count up from 1 to 10, interval 1 second" },
{ "down 1000 to 980", "prefix: 'DOWN', count down from 1000 to 980, interval 0.5 second"},
};
for (const auto& key_value_pair: tdl) { // <--- range based for (const auto&, to save a copy)
const auto& name = key_value_pair.first;
const auto& desc = key_value_pair.second;
std::cout << "NAME: " << name << ", DESC: " << desc << std::endl;
}
return 0;
}
Still Too Old-School: Want Python’s Tuple Unpacking¶
Annoying: manually unpacking
std::pair
‘sfirst
,second
In Python there they have tuple unpacking:
for k, v in tdl.items(): ...
⟶ want that!
const auto&
as alwaysYay, can omit the braces around for-body!
#include <map>
#include <string>
#include <iostream>
int main()
{
using todo_list = std::map<std::string, std::string>;
todo_list tdl{
{ "up 1 to 10", "prefix: 'UP', count up from 1 to 10, interval 1 second" },
{ "down 1000 to 980", "prefix: 'DOWN', count down from 1000 to 980, interval 0.5 second"},
};
for (const auto& [name, desc]: tdl) // <--- unpacking std::pair right into its parts
std::cout << "NAME: " << name << ", DESC: " << desc << std::endl;
return 0;
}
What If To-Do List Items Can Really Do Something? ⟶ Functions¶
Strings “Up, 1 .. 10”, and “Down, 1000 .. 980” are not really unambiguous
Lets write unambiguous functions (
void(*)()
), and run themNew thing:
<chrono>
, and its literals#include <chrono> using namespace std::chrono_literals; std::this_thread::sleep_for(1s);
#include <map>
#include <string>
#include <iostream>
#include <chrono>
#include <thread>
using namespace std::chrono_literals;
void up_1_to_10()
{
for (int i=1; i<=10; i++) {
std::cout << "UP: " << i << std::endl;
std::this_thread::sleep_for(1s);
}
}
void down_1000_to_980()
{
for (int i=1000; i>=980; i--) {
std::cout << "DOWN: " << i << std::endl;
std::this_thread::sleep_for(0.5s);
}
}
int main()
{
using todo_list = std::map<std::string, void(*)()>;
todo_list tdl{
{ "up 1 to 10", up_1_to_10 },
{ "down 1000 to 980", down_1000_to_980},
};
for (const auto& [name, func]: tdl) {
std::cout << "---------------- Running NAME: " << name << std::endl;
func();
}
return 0;
}
Function Pointers Are Old-School ⟶ std::function
¶
Replace
void(*)()
withstd::function
(<functional>
)⟶ can take any callable
#include <map>
#include <string>
#include <iostream>
#include <chrono>
#include <thread>
#include <functional>
using namespace std::chrono_literals;
void up_1_to_10()
{
for (int i=1; i<=10; i++) {
std::cout << "UP: " << i << std::endl;
std::this_thread::sleep_for(1s);
}
}
void down_1000_to_980()
{
for (int i=1000; i>=980; i--) {
std::cout << "DOWN: " << i << std::endl;
std::this_thread::sleep_for(0.5s);
}
}
int main()
{
using todo_list = std::map<std::string, std::function<void()>>; // <---
todo_list tdl{
{ "up 1 to 10", up_1_to_10 },
{ "down 1000 to 980", down_1000_to_980},
};
for (const auto& [name, func]: tdl) {
std::cout << "---------------- Running NAME: " << name << std::endl;
func();
}
return 0;
}
Definitely Not Old-School: Lambda¶
up_1_to_10()
anddown_1000_to_980()
are one-shot functionsMake only sense at one point: part of a todo list
Want to define them where I use them
⟶ Lambda
Note
See how std::function<void()>
can take a lambda, as long as the
signature fits!
#include <map>
#include <string>
#include <iostream>
#include <chrono>
#include <thread>
#include <functional>
using namespace std::chrono_literals;
int main()
{
using todo_list = std::map<std::string, std::function<void()>>;
todo_list tdl{
{ "up 1 to 10",
[](){ // <---
for (int i=1; i<=10; i++) {
std::cout << "UP: " << i << std::endl;
std::this_thread::sleep_for(1s);
}
}
},
{ "down 1000 to 980",
[](){ // <---
for (int i=1000; i>=980; i--) {
std::cout << "DOWN: " << i << std::endl;
std::this_thread::sleep_for(0.5s);
}
}
},
};
for (const auto& [name, func]: tdl) {
std::cout << "---------------- Running NAME: " << name << std::endl;
func();
}
return 0;
}
Inevitable: Threads¶
Threads are not a toy
Far too easy in C++ since 2011
Morph todo-item into
std::pair<std::function<void()>, std::shared_ptr<std::thread>>
Start in a loop
Join in another loop
#include <map>
#include <string>
#include <iostream>
#include <chrono>
#include <thread>
#include <functional>
#include <memory>
using namespace std::chrono_literals;
int main()
{
using todo_item = std::pair<std::function<void()>, std::shared_ptr<std::thread>>;
using todo_list = std::map<std::string, todo_item>;
todo_list tdl{
{ "up 1 to 10",
{
[](){
for (int i=1; i<=10; i++) {
std::cout << "UP: " << i << std::endl;
std::this_thread::sleep_for(1s);
}
},
nullptr
},
},
{ "down 1000 to 980",
{
[](){
for (int i=1000; i>=980; i--) {
std::cout << "DOWN: " << i << std::endl;
std::this_thread::sleep_for(0.5s);
}
},
nullptr
},
},
};
for (auto& [name, item]: tdl) {
std::cout << "---------------- Starting NAME: " << name << std::endl;
item.second = std::make_shared<std::thread>(item.first);
}
for (auto& [name, item]: tdl) {
std::cout << "---------------- Starting NAME: " << name << std::endl;
item.second->join();
}
return 0;
}
Unions? std::variant
!¶
Why use
std::pair<std::function<void()>, std::shared_ptr<std::thread>>
when only either function or thread is active?⟶
std::variant
#include <map>
#include <string>
#include <iostream>
#include <chrono>
#include <thread>
#include <functional>
#include <memory>
#include <variant>
using namespace std::chrono_literals;
int main()
{
using todo_item = std::variant<std::function<void()>, std::shared_ptr<std::thread>>;
using todo_list = std::map<std::string, todo_item>;
todo_list tdl{
{ "up 1 to 10",
[](){
for (int i=1; i<=10; i++) {
std::cout << "UP: " << i << std::endl;
std::this_thread::sleep_for(1s);
}
},
},
{ "down 1000 to 980",
[](){
for (int i=1000; i>=980; i--) {
std::cout << "DOWN: " << i << std::endl;
std::this_thread::sleep_for(0.5s);
}
},
},
};
for (auto& [name, item]: tdl) {
std::cout << "---------------- Starting NAME: " << name << std::endl;
item = std::make_shared<std::thread>(std::get<0>(item));
}
for (auto& [name, item]: tdl) {
std::cout << "---------------- Starting NAME: " << name << std::endl;
std::get<1>(item)->join();
}
return 0;
}
Wrapping All That Into A Class¶
This is getting too big ⟶ encapsulate into
class TodoList
Copying an object that maintains threads shouldn’t be possible.
Copy is possible though: for technical reasons we had to use
std::shared_ptr<std::thread>
= delete
#include <map>
#include <string>
#include <iostream>
#include <chrono>
#include <thread>
#include <functional>
#include <memory>
#include <variant>
using namespace std::chrono_literals;
class TodoList
{
public:
TodoList() = default;
TodoList(const TodoList&) = delete;
TodoList& operator=(const TodoList&) = delete;
void add_item(const std::string& name, std::function<void()> func)
{
_list[name] = todo_item(func);
}
void start()
{
for (auto& [name, item]: _list)
item = std::make_shared<std::thread>(std::get<0>(item));
}
void wait()
{
for (auto& [name, item]: _list)
std::get<1>(item)->join();
}
private:
using todo_item = std::variant<std::function<void()>, std::shared_ptr<std::thread>>;
using todo_list = std::map<std::string, todo_item>;
todo_list _list;
};
int main()
{
TodoList tdl;
tdl.add_item(
"up 1 to 10",
[](){
for (int i=1; i<=10; i++) {
std::cout << "UP: " << i << std::endl;
std::this_thread::sleep_for(1s);
}
});
tdl.add_item(
"down 1000 to 980",
[](){
for (int i=1000; i>=980; i--) {
std::cout << "DOWN: " << i << std::endl;
std::this_thread::sleep_for(0.5s);
}
});
tdl.start();
tdl.wait();
return 0;
}
Architectural Considerations: No Implementation Inheritance Wanted¶
Clean architecture does not do too much implementation inheritance (at least, OO evangelists say so)
Let inhibit that ⟶ make the class
final
#include <map>
#include <string>
#include <iostream>
#include <chrono>
#include <thread>
#include <functional>
#include <memory>
#include <variant>
using namespace std::chrono_literals;
class TodoList final
{
public:
TodoList() = default;
TodoList(const TodoList&) = delete;
TodoList& operator=(const TodoList&) = delete;
void add_item(const std::string& name, std::function<void()> func)
{
_list[name] = todo_item(func);
}
void start()
{
for (auto& [name, item]: _list)
item = std::make_shared<std::thread>(std::get<0>(item));
}
void wait()
{
for (auto& [name, item]: _list)
std::get<1>(item)->join();
}
private:
using todo_item = std::variant<std::function<void()>, std::shared_ptr<std::thread>>;
using todo_list = std::map<std::string, todo_item>;
todo_list _list;
};
int main()
{
TodoList tdl;
tdl.add_item(
"up 1 to 10",
[](){
for (int i=1; i<=10; i++) {
std::cout << "UP: " << i << std::endl;
std::this_thread::sleep_for(1s);
}
});
tdl.add_item(
"down 1000 to 980",
[](){
for (int i=1000; i>=980; i--) {
std::cout << "DOWN: " << i << std::endl;
std::this_thread::sleep_for(0.5s);
}
});
tdl.start();
tdl.wait();
return 0;
}
Wrapping Up: Initializer¶
Lost the ability to initialize a
TodoList
object (⟶add_item()
)Add that
Done!
Note
std::map
member type is std::pair<const keytype,
valuetype>
; take that into account when specifying the
initializer list’s shape.
#include <map>
#include <string>
#include <iostream>
#include <chrono>
#include <thread>
#include <functional>
#include <memory>
#include <variant>
using namespace std::chrono_literals;
class TodoList final
{
public:
TodoList() = default;
TodoList(std::initializer_list<std::pair<const std::string, std::function<void()>>> l)
{
for (const auto& [key, func]: l) // <--- eliminate by pulling std::variant into initializer_list<>
_list[key] = func;
}
TodoList(const TodoList&) = delete;
TodoList& operator=(const TodoList&) = delete;
void add_item(const std::string& name, std::function<void()> func)
{
_list[name] = todo_item(func);
}
void start()
{
for (auto& [name, item]: _list)
item = std::make_shared<std::thread>(std::get<0>(item));
}
void wait()
{
for (auto& [name, item]: _list)
std::get<1>(item)->join();
}
private:
using todo_item = std::variant<std::function<void()>, std::shared_ptr<std::thread>>;
using todo_list = std::map<std::string, todo_item>;
todo_list _list;
};
int main()
{
TodoList tdl{
{"up 1 to 10",
[](){
for (int i=1; i<=10; i++) {
std::cout << "UP: " << i << std::endl;
std::this_thread::sleep_for(1s);
}
}
},
{"down 1000 to 980",
[](){
for (int i=1000; i>=980; i--) {
std::cout << "DOWN: " << i << std::endl;
std::this_thread::sleep_for(0.5s);
}
}
}
};
tdl.start();
tdl.wait();
return 0;
}