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++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;
}
$ ./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 aroundstd::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[]
¶
⟶ A-ha: this is why we had to implement default constructor
We don’t want, though!
Real Container Initialization: Brace Initialization¶
I don’t want
class Item
to have a default constructor (I don’t use it)I don’t want to use std::map::insert() either
I want my map to be
const
, and initialized with content
#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 calledDerived 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 workBy 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 dummyShould 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
for (todo_list::const_iterator it=tdl.begin(); it!= tdl.end(); ++it)
delete it->second;
Explain std::shared_ptr
Explain std::unique_ptr
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
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&
) omittedtdl.begin()
givesconst_iterator
iftdl
isconst
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, obviouslyTransformation 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 doPlain 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