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 in the nature of C++03

  • No 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” initialization

  • std::map is initialized ⟶ could be const!

  • Almost feels like Python: dict([(1, "one"), (2, "two")])

#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 names

  • typedef 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 ‘s first, 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 type

  • Here: deducing the type of std::pair members

  • On 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 for while

  • 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 ‘s first, second

  • In Python there they have tuple unpacking: for k, v in tdl.items(): ...

  • want that!

  • const auto& as always

  • Yay, 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 them

  • New 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(*)() with std::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() and down_1000_to_980() are one-shot functions

  • Make 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;
}