A One-Day Overview Of C++

A one-day ride through C++ for those who can take it. Course format is “trainer hacks/speaks, audience speaks up to comment/ask/discuss”.

The intent of this course [1] is not to teach C++ [2], but rather to give an overview of it to experienced programmers. For example,

  • Different teams who already use C++ come together in the course, and develop a common viewpoint and vocabulary.

  • Those who come from C (or an entirely different language?) want to see what they’re up to.

This is not a slide deck, but rather can be seen as a live-coding “screenplay” that is used by the trainer to not get too far off track. It starts with an old-style (pre C++11) version of a nonsense program, which is continuously modified into something completely different (which does not make much more sense either).

I wrote this up after a number of iterations of the talk (here’s another version of it). C++ cannot be taught in just one day; that seems to be clear to the companies that have booked the courses. (See here for one such course)

C++03 Todo-List

  • Todo-list: a key-value store

  • Using std::map<std::string,std::string> as clumsy as is 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;
}
$ ./c++-intro-overview-oo-todolist-orig
NAME: down 1000 to 980, DESC: prefix: 'DOWN', count down from 1000 to 980, interval 0.5 second
NAME: up 1 to 10, DESC: prefix: 'UP', count up from 1 to 10, interval 1 second

Pitfall: Encapsulate std::map Value In class Item

  • Encapsulate value part of te map into something more approachable, for later extension

  • Obvious implementation of class Item (only a wrapper around std::string)

    class Item
    {
    public:
        Item(const std::string& descr) : _descr(descr) {}
        void doit() const
        {
            std::cout << _descr;
        }
    private:
        std::string _descr;
    };
    
  • Not enough, compiler complains (in its usual painful way) that Item needs a default constructor.

    ... 10 kilometers omitted ...
    /usr/include/c++/13/tuple:2268:9: error: no matching function for call to ‘Item::Item()’
     2268 |         second(std::forward<_Args2>(std::get<_Indexes2>(__tuple2))...)
          |         ^~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
    
  • Not (yet) entirely clear, but adding a default constructor is a workaround

    #include <map>
    #include <string>
    #include <iostream>
    
    class Item
    {
    public:
        Item() = default;                                  // <-- WTF? I don't want this!
        Item(const std::string& descr) : _descr(descr) {}
        void doit() const
        {
            std::cout << _descr;
        }
    private:
        std::string _descr;
    };
    
    int main()
    {
        using todo_list = std::map<std::string, Item>;
    
        todo_list tdl;
        tdl["up 1 to 10"] = Item("prefix: 'UP', count up from 1 to 10, interval 1 second");
        tdl["down 1000 to 980"] = Item("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;
            Item item = it->second;
    
            std::cout << "NAME: " << name << ", ";
            item.doit();
            std::cout << '\n';
        }
    
        return 0;
    }
    

Pitfall: Accessing std::map Using Its operator[]

Real Container Initialization: Brace Initialization

#include <map>
#include <string>
#include <iostream>

class Item
{
public:
    Item(const std::string& descr)
    : _descr(descr) {}                                 // <-- the only ctor!
    void doit() const
    {
        std::cout << _descr;
    }
private:
    std::string _descr;
};

int main()
{
    using todo_list = std::map<std::string, Item>;

    const todo_list tdl = {                            // <-- note the "const"!
        { "up 1 to 10", Item("prefix: 'UP', count up from 1 to 10, interval 1 second") },
        { "down 1000 to 980", Item("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;
        Item item = it->second;

        std::cout << "NAME: " << name << ", ";
        item.doit();
        std::cout << '\n';
    }

    return 0;
}

OOP: Towards The Interface Dogma

Two Kinds Of Items, Two Classes

  • Naive way: implement two non-related classes

    • To the “UP” item class, add a member prefix, for later (slicing)

    • Be naive for that entire section, until we actually understand the dogma

  • Omit constructors and members (there’s only doit() methods)

  • ⟶ wonder how to get two distinct types into the map

  • ⟶ intermediate, non-functional, version

#include <map>
#include <string>
#include <iostream>

class Item_up_1_to_10
{
public:
    Item_up_1_to_10(const std::string& prefix) : _prefix(prefix) {}
    void doit() const
    {
        for (int i=1; i<=10; i++)
            std::cout << _prefix << ", UP: " << i << '\n';
    }
private:
    std::string _prefix;
};

class Item_down_1000_to_980
{
public:
    void doit() const
    {
        for (int i=1000; i>=980; i--)
            std::cout << "DOWN: " << i << '\n';
    }
};

int main()
{
    using todo_list = std::map<std::string, Item>;     // <-- ???

    const todo_list tdl = {
        { "up 1 to 10", Item_up_1_to_10("blah") },
        { "down 1000 to 980", Item_down_1000_to_980() },
    };

    for (todo_list::const_iterator it=tdl.begin(); it!= tdl.end(); ++it) {
        std::string name = it->first;
        Item item = it->second;                        // <-- ???

        std::cout << "NAME: " << name << ", ";
        item.doit();
        std::cout << '\n';
    }

    return 0;
}

Inheritance (Make It Compile, But Not Yet Work)

  • Radically, just to get objects into the map: derive from base class Item

  • What to do in base class? ⟶ output nonsense

  • Talk about whats going on (see next slide

#include <map>
#include <string>
#include <iostream>

class Item
{
public:
    void doit() const
    {
        std::cout << "don't know what to do";
    }
};

class Item_up_1_to_10 : public Item
{
public:
    Item_up_1_to_10(const std::string& prefix) : _prefix(prefix) {}
    void doit() const
    {
        for (int i=1; i<=10; i++)
            std::cout << _prefix << ", UP: " << i << '\n';
    }
private:
    std::string _prefix;
};

class Item_down_1000_to_980 : public Item
{
public:
    void doit() const
    {
        for (int i=1000; i>=980; i--)
            std::cout << "DOWN: " << i << '\n';
    }
};

int main()
{
    using todo_list = std::map<std::string, Item>;       // <-- base class

    const todo_list tdl = {
        { "up 1 to 10", Item_up_1_to_10("blah") },       // <-- copy: derived onto base
        { "down 1000 to 980", Item_down_1000_to_980() }, // <-- copy: derived onto base
    };

    for (todo_list::const_iterator it=tdl.begin(); it!= tdl.end(); ++it) {
        std::string name = it->first;
        Item item = it->second;                          // <-- copy: base onto base

        std::cout << "NAME: " << name << ", ";
        item.doit();
        std::cout << '\n';
    }

    return 0;
}
  • Only Item::doit() is called

  • Derived functionality not reached

$ ./c++-intro-overview-oo-todolist-items-non-working
NAME: down 1000 to 980, don't know what to do
NAME: up 1 to 10, don't know what to do

Analysis: The Perils Of Inheritance - Slicing

  • Clarify: comment out todolist, and explain sideways

  • C++ permits one to copy an object of derived type onto an object of base type

  • Question: what if derived has additional members? Adds to the size of base?

  • slicing

  • ⟶ mostly undesired, but legal

#include <map>
#include <string>
#include <iostream>

class Item
{
public:
    void doit() const
    {
        std::cout << "don't know what to do";
    }
};

class Item_up_1_to_10 : public Item
{
public:
    Item_up_1_to_10(const std::string& prefix) : _prefix(prefix) {}
    void doit() const
    {
        for (int i=1; i<=10; i++)
            std::cout << _prefix << ", UP: " << i << '\n';
    }
private:
    std::string _prefix;
};

int main()
{
    Item_up_1_to_10 derived("blah");
    // derived.doit();                                 // <-- STEP 1: works on derived instance, obviously

    Item base;
    base = derived;                                    // <-- STEP 2: *convert* (omit all that base hasn't)
    base.doit();                                       //             *not* correct (prints base nonsense)

    return 0;
}

Analysis: The Perils Of Inheritance - Automatic Pointer Type Conversion

  • Better than copying objects of different sizes onto each other: automatic type conversion

  • The Plan

  • Still not perfect

  • Nothing gets lost: only pointers are copied, the information in the derived object is still there

  • Still not working though

  • ⟶ Derived class functionality not reachable

#include <map>
#include <string>
#include <iostream>

class Item
{
public:
    void doit() const
    {
        std::cout << "don't know what to do";
    }
};

class Item_up_1_to_10 : public Item
{
public:
    Item_up_1_to_10(const std::string& prefix) : _prefix(prefix) {}
    void doit() const
    {
        for (int i=1; i<=10; i++)
            std::cout << _prefix << ", UP: " << i << '\n';
    }
private:
    std::string _prefix;
};

int main()
{
    Item_up_1_to_10 derived("blah");

    Item* base;
    base = &derived;                                    // <-- converted to base *pointer*
    base->doit();                                       //     still prints base nonsense

    return 0;
}

Key To Polymorphism: virtual

  • Makes base->doit() magically work

  • By adding runtime type information

  • dynamic method dispatch, depending on concrete type

  • ⟶ Extension mechanism

#include <map>
#include <string>
#include <iostream>

class Item
{
public:
    virtual void doit() const
    {
        std::cout << "don't know what to do";
    }
};

class Item_up_1_to_10 : public Item
{
public:
    Item_up_1_to_10(const std::string& prefix) : _prefix(prefix) {}
    void doit() const
    {
        for (int i=1; i<=10; i++)
            std::cout << _prefix << ", UP: " << i << '\n';
    }
private:
    std::string _prefix;
};

int main()
{
    Item_up_1_to_10 derived("blah");

    Item* base;
    base = &derived;
    base->doit();                                       // <-- *dynamic dispatch*

    return 0;
}

Pitfall: Incorrectly Implement Derived Class Method

  • Someone comes along and only understands void doit()

  • (const not considered important to many)

  • ⟶ New method; different from void doit() const

#include <map>
#include <string>
#include <iostream>

class Item
{
public:
    virtual void doit() const
    {
        std::cout << "don't know what to do";
    }
};

class YetAnotherItem : public Item
{
public:
    void doit()
    {
        std::cout << "Doing yet another thing";
    }
};

int main()
{
    YetAnotherItem derived;

    Item* base;
    base = &derived;
    base->doit();

    return 0;
}

Solution: That’s What override Is There For

  • override attached by user of base class (implementor of derived class)

  • Makes the intent clear

#include <map>
#include <string>
#include <iostream>

class Item
{
public:
    virtual void doit() const
    {
        std::cout << "don't know what to do";
    }
};

class YetAnotherItem : public Item
{
public:
    void doit() override
    {
        std::cout << "Doing yet another thing";
    }
};

int main()
{
    YetAnotherItem derived;

    Item* base;
    base = &derived;
    base->doit();

    return 0;
}
todolist-sideway-override.cpp:17:10: error: ‘void YetAnotherItem::doit()’ marked ‘override’, but does not override
   17 |     void doit() override
      |          ^~~~

Pure Virtual Methods (“I Don’t Know What class Item Would Do”)

  • Implementation of Item::doit() is purely dummy

  • Should not be there in the first place

  • Pure virtual method: virtual void doit() const = 0

  • ⟶ In fact, is not there

  • Forces derived classes to implement it

  • Btw., makes override redundant if only single-level inheritance is used

#include <map>
#include <string>
#include <iostream>

class Item
{
public:
    virtual void doit() const = 0;
};

class Item_up_1_to_10 : public Item
{
public:
    Item_up_1_to_10(const std::string& prefix) : _prefix(prefix) {}
    void doit() const override                         // <-- override is redundant (base is pure)
    {
        for (int i=1; i<=10; i++)
            std::cout << _prefix << ", UP: " << i << '\n';
    }
private:
    std::string _prefix;
};

int main()
{
    Item_up_1_to_10 derived("blah");

    Item* base;
    base = &derived;
    base->doit();

    return 0;
}

Pitfall: Derived Class Destructor (1)

  • Destructors are special

  • Lets be naive …

#include <map>
#include <string>
#include <cstring>
#include <iostream>

class Item
{
public:
    virtual void doit() const = 0;
};

class AllocatingItem : public Item
{
public:
    AllocatingItem(const char* descr)
    : _descr(new char[strlen(descr)+1])
    {
        strcpy(_descr, descr);
    }
    ~AllocatingItem()
    {
        delete[] _descr;
    }
    void doit() const override
    {
        std::cout << "Allocated space for: " << _descr;
    }

private:
    char* _descr;
};

int main()
{
    AllocatingItem derived("blah");

    Item* base;
    base = &derived;
    base->doit();

    return 0;
}
  • Runs as expected

  • No memory leaks

$ ./c++-intro-overview-oo-todolist-sideway-nonvirtual-dtor
Allocated space for: blah(jfasch-home)
$ valgrind ./c++-intro-overview-oo-todolist-sideway-nonvirtual-dtor
...
HEAP SUMMARY:
    in use at exit: 0 bytes in 0 blocks
...

Pitfall: Derived Class Destructor (2)

  • But what if we call destructor via base class?

  • Much like with methods ⟶ derived destructor not reached

  • virtual destructor

  • Cannot be pure virtual

    • Destructors are special

    • Called from most derived upwards to base

    • ⟶ Interface must have “do nothing” dtor implementation

#include <map>
#include <string>
#include <cstring>
#include <iostream>

class Item
{
public:
    virtual ~Item() = default;
    virtual void doit() const = 0;
};

class AllocatingItem : public Item
{
public:
    AllocatingItem(const char* descr)
    : _descr(new char[strlen(descr)+1])
    {
        strcpy(_descr, descr);
    }
    ~AllocatingItem() override
    {
        delete[] _descr;
    }
    void doit() const override
    {
        std::cout << "Allocated space for: " << _descr;
    }

private:
    char* _descr;
};

int main()
{
    Item* base = new AllocatingItem("blah");
    base->doit();
    delete base;

    return 0;
}

The Interface, Put Dogmatically

class Interface
{
public:
    virtual ~Implementation() = default;
    virtual void doit() const = 0;
};

class Implementation : public Interface
{
public:
    ~Implementation() override ...                     // <-- optional
    void doit() const override ...                     // <-- mandatory
};

Wrap Up: Polymorpic Todolist

  • Long story short: here’s the final polymorphic version

  • Note the extra cleanup step at the end ⟶ fragile (easily forgotten)

#include <map>
#include <string>
#include <iostream>

class Item
{
public:
    virtual ~Item() = default;
    virtual void doit() const = 0;
};

class Item_up_1_to_10 : public Item
{
public:
    Item_up_1_to_10(const std::string& prefix) : _prefix(prefix) {}
    void doit() const override
    {
        for (int i=1; i<=10; i++)
            std::cout << _prefix << ", UP: " << i << '\n';
    }
private:
    std::string _prefix;
};

class Item_down_1000_to_980 : public Item
{
public:
    void doit() const override
    {
        for (int i=1000; i>=980; i--)
            std::cout << "DOWN: " << i << '\n';
    }
};

int main()
{
    using todo_list = std::map<std::string, Item*>;            // <-- pointer to interface

    const todo_list tdl = {
        { "up 1 to 10", new Item_up_1_to_10("blah") },         // <-- dynamic allocation
        { "down 1000 to 980", new Item_down_1000_to_980() },   // <-- dynamic allocation
    };

    for (todo_list::const_iterator it=tdl.begin(); it!= tdl.end(); ++it) {
        std::string name = it->first;
        const Item* item = it->second;

        std::cout << "NAME: " << name << ", ";
        item->doit();                                          // <-- polymorphic use
        std::cout << '\n';
    }

    for (todo_list::const_iterator it=tdl.begin(); it!= tdl.end(); ++it)
        delete it->second;                                     // <-- polymorphic dtor call

    return 0;
}

Memory Management: Smart Pointers (Showing The Options)

  • Plan: no manual memory management

BAD
for (todo_list::const_iterator it=tdl.begin(); it!= tdl.end(); ++it)
    delete it->second;

Memory Management: std::unique_ptr<> In TodoList ⟶ No

  • Try to use it (failed code below)

  • Explain std::initializer_list<>

  • Show std::map(std::initializer_list<value_type>) (here) ⟶ by copy

  • std::unique_ptr<> is move only

  • std::unique_ptr<> unusable in brace initialization

#include <map>
#include <string>
#include <memory>
#include <iostream>

class Item
{
public:
    virtual ~Item() = default;
    virtual void doit() const = 0;
};

class Item_up_1_to_10 : public Item
{
public:
    Item_up_1_to_10(const std::string& prefix) : _prefix(prefix) {}
    void doit() const override
    {
        for (int i=1; i<=10; i++)
            std::cout << _prefix << ", UP: " << i << '\n';
    }
private:
    std::string _prefix;
};

class Item_down_1000_to_980 : public Item
{
public:
    void doit() const override
    {
        for (int i=1000; i>=980; i--)
            std::cout << "DOWN: " << i << '\n';
    }
};

int main()
{
    using todo_list = std::map<std::string, std::unique_ptr<Item>>;          // <-- pointer to base

    const todo_list tdl = {
        { "up 1 to 10", std::make_unique<Item_up_1_to_10>("blah") },         // <-- "pointer" conversion
        { "down 1000 to 980", std::make_unique<Item_down_1000_to_980>() },   // <-- "pointer" conversion
    };

    for (todo_list::const_iterator it=tdl.begin(); it!= tdl.end(); ++it) {
        std::string name = it->first;
        const std::unique_ptr<Item>& item = it->second;                      // <-- pointer to base

        std::cout << "NAME: " << name << ", ";
        item->doit();                                                        // <-- "behaves-like-a" pointer
        std::cout << '\n';
    }

                                                                             // <-- automatic cleanup

    return 0;
}

A Short Deviation: Move Semantics

  • At that time, a little deviation into move semantics is in order

  • ⟶ This helps understand the compiler when he tries to just say “no” but fails

Memory Management: std::shared_ptr<> In TodoList

  • I do want brace initialization

  • And I don’t want to do manual memory management

  • ⟶ The only viable option is to use std::shared_ptr<>

#include <map>
#include <string>
#include <memory>
#include <iostream>

class Item
{
public:
    virtual ~Item() = default;
    virtual void doit() const = 0;
};

class Item_up_1_to_10 : public Item
{
public:
    Item_up_1_to_10(const std::string& prefix) : _prefix(prefix) {}
    void doit() const override
    {
        for (int i=1; i<=10; i++)
            std::cout << _prefix << ", UP: " << i << '\n';
    }
private:
    std::string _prefix;
};

class Item_down_1000_to_980 : public Item
{
public:
    void doit() const override
    {
        for (int i=1000; i>=980; i--)
            std::cout << "DOWN: " << i << '\n';
    }
};

int main()
{
    using todo_list = std::map<std::string, std::shared_ptr<Item>>;          // <-- pointer to base

    const todo_list tdl = {
        { "up 1 to 10", std::make_shared<Item_up_1_to_10>("blah") },         // <-- "pointer" conversion
        { "down 1000 to 980", std::make_shared<Item_down_1000_to_980>() },   // <-- "pointer" conversion
    };

    for (todo_list::const_iterator it=tdl.begin(); it!= tdl.end(); ++it) {
        std::string name = it->first;
        const std::shared_ptr<Item>& item = it->second;                      // <-- pointer to base

        std::cout << "NAME: " << name << ", ";
        item->doit();                                                        // <-- "behaves-like-a" pointer
        std::cout << '\n';
    }

                                                                             // <-- automatic cleanup

    return 0;
}

Readability: Long Type Names ⟶ auto

  • std::map<std::string, std::shared_ptr<Item>::const_iterator

  • … and more

  • Compiler knows what the type of tdl.begin() is

  • auto

#include <map>
#include <string>
#include <memory>
#include <iostream>

class Item
{
public:
    virtual ~Item() = default;
    virtual void doit() const = 0;
};

class Item_up_1_to_10 : public Item
{
public:
    Item_up_1_to_10(const std::string& prefix) : _prefix(prefix) {}
    void doit() const override
    {
        for (int i=1; i<=10; i++)
            std::cout << _prefix << ", UP: " << i << '\n';
    }
private:
    std::string _prefix;
};

class Item_down_1000_to_980 : public Item
{
public:
    void doit() const override
    {
        for (int i=1000; i>=980; i--)
            std::cout << "DOWN: " << i << '\n';
    }
};

int main()
{
    using todo_list = std::map<std::string, std::shared_ptr<Item>>;

    const todo_list tdl = {
        { "up 1 to 10", std::make_shared<Item_up_1_to_10>("blah") },
        { "down 1000 to 980", std::make_shared<Item_down_1000_to_980>() },
    };

    for (auto it=tdl.begin(); it!= tdl.end(); ++it) {
        auto name = it->first;                                               // <-- copy!
        auto item = it->second;                                              // <-- copy!

        std::cout << "NAME: " << name << ", ";
        item->doit();
        std::cout << '\n';
    }

    return 0;
}

Pitfalls: auto

  • auto is only the base type (const and &) omitted

  • tdl.begin() gives const_iterator if tdl is const

  • Non const otherwise

  • tdl.cbegin()

#include <map>
#include <string>
#include <memory>
#include <iostream>

class Item
{
public:
    virtual ~Item() = default;
    virtual void doit() const = 0;
};

class Item_up_1_to_10 : public Item
{
public:
    Item_up_1_to_10(const std::string& prefix) : _prefix(prefix) {}
    void doit() const override
    {
        for (int i=1; i<=10; i++)
            std::cout << _prefix << ", UP: " << i << '\n';
    }
private:
    std::string _prefix;
};

class Item_down_1000_to_980 : public Item
{
public:
    void doit() const override
    {
        for (int i=1000; i>=980; i--)
            std::cout << "DOWN: " << i << '\n';
    }
};

int main()
{
    using todo_list = std::map<std::string, std::shared_ptr<Item>>;

    const todo_list tdl = {
        { "up 1 to 10", std::make_shared<Item_up_1_to_10>("blah") },
        { "down 1000 to 980", std::make_shared<Item_down_1000_to_980>() },
    };

    for (auto it=tdl.begin(); it!= tdl.end(); ++it) {
        const auto& name = it->first;                                        // <-- better
        const auto& item = it->second;                                       // <-- better

        std::cout << "NAME: " << name << ", ";
        item->doit();
        std::cout << '\n';
    }

    return 0;
}

Readability: Tuple Unpacking (Err, Structured Binding)

  • auto is required, obviously

  • Transformation is trivial

#include <map>
#include <string>
#include <memory>
#include <iostream>

class Item
{
public:
    virtual ~Item() = default;
    virtual void doit() const = 0;
};

class Item_up_1_to_10 : public Item
{
public:
    Item_up_1_to_10(const std::string& prefix) : _prefix(prefix) {}
    void doit() const override
    {
        for (int i=1; i<=10; i++)
            std::cout << _prefix << ", UP: " << i << '\n';
    }
private:
    std::string _prefix;
};

class Item_down_1000_to_980 : public Item
{
public:
    void doit() const override
    {
        for (int i=1000; i>=980; i--)
            std::cout << "DOWN: " << i << '\n';
    }
};

int main()
{
    using todo_list = std::map<std::string, std::shared_ptr<Item>>;

    const todo_list tdl = {
        { "up 1 to 10", std::make_shared<Item_up_1_to_10>("blah") },
        { "down 1000 to 980", std::make_shared<Item_down_1000_to_980>() },
    };

    for (auto it=tdl.begin(); it!= tdl.end(); ++it) {
        const auto& [name, item] = *it;                                      // <-- structure bound to variables

        std::cout << "NAME: " << name << ", ";
        item->doit();
        std::cout << '\n';
    }

    return 0;
}

Range Based for

  • Iterator based loops? Pointer arithmetic? ⟶ NO

  • Element based iteration (Python’s for, C# foreach)

  • … in combination with auto

#include <map>
#include <string>
#include <memory>
#include <iostream>

class Item
{
public:
    virtual ~Item() = default;
    virtual void doit() const = 0;
};

class Item_up_1_to_10 : public Item
{
public:
    Item_up_1_to_10(const std::string& prefix) : _prefix(prefix) {}
    void doit() const override
    {
        for (int i=1; i<=10; i++)
            std::cout << _prefix << ", UP: " << i << '\n';
    }
private:
    std::string _prefix;
};

class Item_down_1000_to_980 : public Item
{
public:
    void doit() const override
    {
        for (int i=1000; i>=980; i--)
            std::cout << "DOWN: " << i << '\n';
    }
};

int main()
{
    using todo_list = std::map<std::string, std::shared_ptr<Item>>;

    const todo_list tdl = {
        { "up 1 to 10", std::make_shared<Item_up_1_to_10>("blah") },
        { "down 1000 to 980", std::make_shared<Item_down_1000_to_980>() },
    };

    for (const auto& [name, item]: tdl) {
        std::cout << "NAME: " << name << ", ";
        item->doit();
        std::cout << '\n';
    }

    return 0;
}

Full Classes Hierarchy For One Method doit()NO (Lambdas)

  • Interfaces are heavyweight

    • Readability suffers

    • Code bloat through virtual (RTTI - Run Time Type Information)

  • Show what a functor is (class with overloaded operator()()) (see here)

  • Turn functor into lambda

  • ⟶ Captures vs. functor members

  • More syntax: Lambda: More Capturing

  • Show what a std::function<> can do

    • Plain old function

    • Functor

    • Lambda

Wrap Up: TodoList, De-Overengineered

  • Transform classes to lambdas

  • Interface gone

  • Big fat class body gone

  • Code bloat gone

#include <map>
#include <string>
#include <functional>
#include <iostream>

int main()
{
    using todo_list = std::map<std::string, std::function<void()>;

    const todo_list tdl = {
        { "up 1 to 10",
          [prefix="blah"](){                                                 // <-- functor with one member 'prefix'
              for (int i=1; i<=10; i++)
                  std::cout << _prefix << ", UP: " << i << '\n';
          }
        },
        { "down 1000 to 980",
          [](){                                                              // <-- functo without a member
              for (int i=1000; i>=980; i--)
                  std::cout << "DOWN: " << i << '\n';
          }
        },
    };

    for (const auto& [name, item]: tdl) {
        std::cout << "NAME: " << name << ", ";
        item();                                                              // <-- "behaves-like-a" function
        std::cout << '\n';
    }

    return 0;
}

Footnotes