Design Patterns in
Modern C++, Part I
Dmitri Nesteruk
dmitrinеstеruk@gmаil.соm
@dnesteruk
What’s In This Talk?
• Examples of patterns and approaches in OOP design
• Adapter
• Composite
• Specification pattern/OCP
• Fluent and Groovy-style builders
• Maybe monad
Adapter
STL String Complaints
• Making a string is easy
string s{“hello world”};
• Getting its constituent parts is not
vector<strings> words;
boost::split(words, s, boost::is_any_of(“ “));
• Instead I would prefer
auto parts = s.split(“ “);
• It should work with “hello world”
• Maybe some other goodies, e.g.
• Hide size()
• Have length() as a property, not a function
Basic Adapter
class String {
string s;
public:
String(const string &s) : s{ s } { }
};
Implement Split
class String {
string s;
public:
String(const string &s) : s{ s } { }
vector<string> split(string input)
{
vector<string> result;
boost::split(result, s,
boost::is_any_of(input), boost::token_compress_on);
return result;
}
};
Length Proxying
class String {
string s;
public:
String(const string &s) : s{ s } { }
vector<string> split(string input);
size_t get_length() const { return s.length(); }
};
Length Proxying
class String {
string s;
public:
String(const string &s) : s{ s } { }
vector<string> split(string input);
size_t get_length() const { return s.length(); }
// non-standard!
__declspec(property(get = get_length)) size_t length;
};
String Wrapper Usage
String s{ "hello world" };
cout << "string has " <<
s.length << " characters" << endl;
auto words = s.split(" ");
for (auto& word : words)
cout << word << endl;
Adapter Summary
• Aggregate objects (or keep a reference)
• Can aggregate more than one
• E.g., string and formatting
• Replicate the APIs you want (e.g., length)
• Miss out on the APIs you don’t need
• Add your own features :)
Composite
Scenario
• Neurons connect to other
neurons
• Neuron layers are collections of
neurons
• These two need to be
connectable
Scenario
struct Neuron
{
vector<Neuron*> in, out;
unsigned int id;
Neuron()
{
static int id = 1;
this->id = id++;
}
}
Scenario
struct NeuronLayer : vector<Neuron>
{
NeuronLayer(int count)
{
while (count-- > 0)
emplace_back(Neuron{});
}
}
State Space Explosition
• void connect_to(Neuron& other)
{
out.push_back(&other);
other.in.push_back(this);
}
• Unfortunately, we need 4 functions
• Neuron to Neuron
• Neuron to NeuronLayer
• NeuronLayer to Neuron
• NeuronLayer to NeuronLayer
One Function Solution?
• Simple: treat Neuron as NeuronLayer of size 1
• Not strictly correct
• Does not take into account other concepts (e.g.,NeuronRing)
• Better: expose a single Neuron in an iterable fashion
• Other programming languages have interfaces for iteration
• E.g., C# IEnumerable<T>
• yield keyword
• C++ does duck typing
• Expects begin/end pair
• One function solution not possible, but…
Generic Connection Function
struct Neuron
{
...
template <typename T> void connect_to(T& other)
{
for (Neuron& to : other)
connect_to(to);
}
template<> void connect_to<Neuron>(Neuron& other)
{
out.push_back(&other);
other.in.push_back(this);
}
};
Generic Connection Function
struct NeuronLayer : vector<Neuron>
{
…
template <typename T> void connect_to(T& other)
{
for (Neuron& from : *this)
for (Neuron& to : other)
from.connect_to(to);
}
};
How to Iterate on a Single Value?
struct Neuron
{
…
Neuron* begin() { return this; }
Neuron* end() { return this + 1; }
};
API Usage
Neuron n, n2;
NeuronLayer nl, nl2;
n.connect_to(n2);
n.connect_to(nl);
nl.connect_to(n);
nl.connect_to(nl2);
Specification Pattern and the OCP
Open-Closed Principle
• Open for extension, closed for modification
• Bad: jumping into old code to change a stable, functioning system
• Good: making things generic enough to be externally extensible
• Example: product filtering
Scenario
enum class Color { Red, Green, Blue };
enum class Size { Small, Medium, Large };
struct Product
{
std::string name;
Color color;
Size size;
};
Filtering Products
struct ProductFilter
{
typedef std::vector<Product*> Items;
static Items by_color(Items items, Color color)
{
Items result;
for (auto& i : items)
if (i->color == color)
result.push_back(i);
return result;
}
}
Filtering Products
struct ProductFilter
{
typedef std::vector<Product*> Items;
static Items by_color(Items items, Color color) { … }
static Items by_size(Items items, Size size)
{
Items result;
for (auto& i : items)
if (i->size == size)
result.push_back(i);
return result;
}
}
Filtering Products
struct ProductFilter
{
typedef std::vector<Product*> Items;
static Items by_color(Items items, Color color) { … }
static Items by_size(Items items, Size size) { … }
static Items by_color_and_size(Items items, Size size, Color color)
{
Items result;
for (auto& i : items)
if (i->size == size && i->color == color)
result.push_back(i);
return result;
}
}
Violating OCP
• Keep having to rewrite existing code
• Assumes it is even possible (i.e. you have access to source code)
• Not flexible enough (what about other criteria?)
• Filtering by X or Y or X&Y requires 3 functions
• More complexity -> state space explosion
• Specification pattern to the rescue!
ISpecification and IFilter
template <typename T> struct ISpecification
{
virtual bool is_satisfied(T* item) = 0;
};
template <typename T> struct IFilter
{
virtual std::vector<T*> filter(
std::vector<T*> items,
ISpecification<T>& spec) = 0;
};
A Better Filter
struct ProductFilter : IFilter<Product>
{
typedef std::vector<Product*> Products;
Products filter(
Products items,
ISpecification<Product>& spec) override
{
Products result;
for (auto& p : items)
if (spec.is_satisfied(p))
result.push_back(p);
return result;
}
};
Making Specifications
struct ColorSpecification : ISpecification<Product>
{
Color color;
explicit ColorSpecification(const Color color)
: color{color} { }
bool is_satisfied(Product* item) override {
return item->color == color;
}
}; // same for SizeSpecification
Improved Filter Usage
Product apple{ "Apple", Color::Green, Size::Small };
Product tree { "Tree", Color::Green, Size::Large };
Product house{ "House", Color::Blue, Size::Large };
std::vector<Product*> all{ &apple, &tree, &house };
ProductFilter pf;
ColorSpecification green(Color::Green);
auto green_things = pf.filter(all, green);
for (auto& x : green_things)
std::cout << x->name << " is green" << std::endl;
Filtering on 2..N criteria
• How to filter by size and color?
• We don’t want a SizeAndColorSpecification
• State space explosion
• Create combinators
• A specification which combines two other specifications
• E.g., AndSpecification
AndSpecification Combinator
template <typename T> struct AndSpecification : ISpecification<T>
{
ISpecification<T>& first;
ISpecification<T>& second;
AndSpecification(ISpecification<T>& first,
ISpecification<T>& second)
: first{first}, second{second} { }
bool is_satisfied(T* item) override
{
return first.is_satisfied(item) && second.is_satisfied(item);
}
};
Filtering by Size AND Color
ProductFilter pf;
ColorSpecification green(Color::Green);
SizeSpecification big(Size::Large);
AndSpecification<Product> green_and_big{ big, green };
auto big_green_things = pf.filter(all, green_and_big);
for (auto& x : big_green_things)
std::cout << x->name << " is big and green" << std::endl;
Specification Summary
• Simple filtering solution is
• Too difficult to maintain, violates OCP
• Not flexible enough
• Abstract away the specification interface
• bool is_satisfied_by(T something)
• Abstract away the idea of filtering
• Input items + specification  set of filtered items
• Create combinators (e.g., AndSpecification) for combining multiple
specifications
Fluent and Groovy-Style Builders
Scenario
• Consider the construction of structured data
• E.g., an HTML web page
• Stuctured and formalized
• Rules (e.g., P cannot contain another P)
• Can we provide an API for building these?
Building a Simple HTML List
// <ul><li>hello</li><li>world</li></ul>
string words[] = { "hello", "world" };
ostringstream oss;
oss << "<ul>";
for (auto w : words)
oss << " <li>" << w << "</li>";
oss << "</ul>";
printf(oss.str().c_str());
HtmlElement
struct HtmlElement
{
string name;
string text;
vector<HtmlElement> elements;
const size_t indent_size = 2;
string str(int indent = 0) const; // pretty-print
}
Html Builder (non-fluent)
struct HtmlBuilder
{
HtmlElement root;
HtmlBuilder(string root_name) { root.name = root_name; }
void add_child(string child_name, string child_text)
{
HtmlElement e{ child_name, child_text };
root.elements.emplace_back(e);
}
string str() { return root.str(); }
}
Html Builder (non-fluent)
HtmlBuilder builder{"ul"};
builder.add_child("li", "hello")
builder.add_child("li", "world");
cout << builder.str() << endl;
Making It Fluent
struct HtmlBuilder
{
HtmlElement root;
HtmlBuilder(string root_name) { root.name = root_name; }
HtmlBuilder& add_child(string child_name, string child_text)
{
HtmlElement e{ child_name, child_text };
root.elements.emplace_back(e);
return *this;
}
string str() { return root.str(); }
}
Html Builder
HtmlBuilder builder{"ul"};
builder.add_child("li", "hello").add_child("li", "world");
cout << builder.str() << endl;
Associate Builder & Object Being Built
struct HtmlElement
{
static HtmlBuilder build(string root_name)
{
return HtmlBuilder{root_name};
}
};
// usage:
HtmlElement::build("ul")
.add_child_2("li", "hello").add_child_2("li", "world");
Groovy-Style Builders
• Express the structure of the HTML in code
• No visible function calls
• UL {
LI {“hello”},
LI {“world”}
}
• Possible in C++ using uniform initialization
Tag (= HTML Element)
struct Tag
{
string name;
string text;
vector<Tag> children;
vector<pair<string, string>> attributes;
protected:
Tag(const std::string& name, const std::string& text)
: name{name}, text{text} { }
Tag(const std::string& name, const std::vector<Tag>& children)
: name{name}, children{children} { }
}
Paragraph
struct P : Tag
{
explicit P(const std::string& text)
: Tag{"p", text}
{
}
P(std::initializer_list<Tag> children)
: Tag("p", children)
{
}
};
Image
struct IMG : Tag
{
explicit IMG(const std::string& url)
: Tag{"img", ""}
{
attributes.emplace_back(make_pair("src", url));
}
};
Example Usage
std::cout <<
P {
IMG {"http://pokemon.com/pikachu.png"}
}
<< std::endl;
Facet Builders
• An HTML element has different facets
• Attributes, inner elements, CSS definitions, etc.
• A Person class might have different facets
• Address
• Employment information
• Thus, an object might necessitate several builders
Personal/Work Information
class Person
{
// address
std::string street_address, post_code, city;
// employment
std::string company_name, position;
int annual_income = 0;
Person() {} // private!
}
Person Builder (Exposes Facet Builders)
class PersonBuilder
{
Person p;
protected:
Person& person;
explicit PersonBuilder(Person& person)
: person{ person } { }
public:
PersonBuilder() : person{p} { }
operator Person() { return std::move(person); }
// builder facets
PersonAddressBuilder lives();
PersonJobBuilder works();
}
Person Builder (Exposes Facet Builders)
class PersonBuilder
{
Person p;
protected:
Person& person;
explicit PersonBuilder(Person& person)
: person{ person } { }
public:
PersonBuilder() : person{p} { }
operator Person() { return std::move(person); }
// builder facets
PersonAddressBuilder lives();
PersonJobBuilder works();
}
Person Builder Facet Functions
PersonAddressBuilder PersonBuilder::lives()
{
return PersonAddressBuilder{ person };
}
PersonJobBuilder PersonBuilder::works()
{
return PersonJobBuilder{ person };
}
Person Address Builder
class PersonAddressBuilder : public PersonBuilder
{
typedef PersonAddressBuilder Self;
public:
explicit PersonAddressBuilder(Person& person)
: PersonBuilder{ person } { }
Self& at(std::string street_address)
{
person.street_address = street_address;
return *this;
}
Self& with_postcode(std::string post_code);
Self& in(std::string city);
};
Person Job Builder
class PersonJobBuilder : public PersonBuilder
{
typedef PersonJobBuilder Self;
public:
explicit PersonJobBuilder(Person& person)
: PersonBuilder{ person } { }
Self& at(std::string company_name);
Self& as_a(std::string position);
Self& earning(int annual_income);
};
Back to Person
class Person
{
// fields
public:
static PersonBuilder create();
friend class PersonBuilder;
friend class PersonAddressBuilder;
friend class PersonJobBuilder;
};
Final Person Builder Usage
Person p = Person::create()
.lives().at("123 London Road")
.with_postcode("SW1 1GB")
.in("London")
.works().at("PragmaSoft")
.as_a("Consultant")
.earning(10e6);
Maybe Monad
Presence or Absence
• Different ways of expressing absence of value
• Default-initialized value
• string s; // there is no ‘null string’
• Null value
• Address* address;
• Not-yet-initialized smart pointer
• shared_ptr<Address> address
• Idiomatic
• boost::optional
Monads
• Design patterns in functional programming
• First-class function support
• Related concepts
• Algebraic data types
• Pattern matching
• Implementable to some degree in C++
• Functional objects/lambdas
Scenario
struct Address
{
char* house_name; // why not string?
}
struct Person
{
Address* address;
}
Print House Name, If Any
void print_house_name(Person* p)
{
if (p != nullptr &&
p->address != nullptr &&
p->address->house_name != nullptr)
{
cout << p->address->house_name << endl;
}
}
Maybe Monad
• Encapsulate the ‘drill down’ aspect of code
• Construct a Maybe<T> which keeps context
• Context: pointer to evaluated element
• person -> address -> name
• While context is non-null, we drill down
• If context is nullptr, propagation does not happen
• All instrumented using lambdas
Maybe<T>
template <typename T>
struct Maybe {
T* context;
Maybe(T *context) : context(context) { }
};
// but, given Person* p, we cannot make a ‘new Maybe(p)’
template <typename T> Maybe<T> maybe(T* context)
{
return Maybe<T>(context);
}
Usage So Far
void print_house_name(Person* p)
{
maybe(p). // now drill down :)
}
Maybe::With
template <typename T> struct Maybe
{
...
template <typename TFunc>
auto With(TFunc evaluator)
{
if (context == nullptr)
return ??? // cannot return maybe(nullptr) :(
return maybe(evaluator(context));
};
}
What is ???
• In case of failure, we need to return Maybe<U>
• But the type of U should be the return type of evaluator
• But evaluator returns U* and we need U
• Therefore…
• return Maybe<
typename remove_pointer<
decltype(evaluator(context))
>::type>(nullptr);
Maybe::With Finished
template <typename TFunc>
auto With(TFunc evaluator)
{
if (context == nullptr)
return Maybe<typename remove_pointer<
decltype(evaluator(context))>::type>(nullptr);
return maybe(evaluator(context));
};
Usage So Far
void print_house_name(Person* p)
{
maybe(p) // now drill down :)
.With([](auto x) { return x->address; })
.With([](auto x) { return x->house_name; })
. // print here (if context is not null)
}
Maybe::Do
template <typename TFunc>
auto Do(TFunc action)
{
// if context is OK, perform action on it
if (context != nullptr) action(context);
// no context transition, so...
return *this;
}
How It Works
• print_house_name(nullptr)
• Context is null from the outset and continues to be null
• Nothing happens in the entire evaluation chain
• Person p; print_house_name(&p);
• Context is Person, but since Address is null, it becomes null henceforth
• Person p;
p->Address = new Address;
p->Address->HouseName = “My Castle”;
print_house_name(&p);
• Everything works and we get our printout
Maybe Monad Summary
• Example is not specific to nullptr
• E.g., replace pointers with boost::optional
• Default-initialized types are harder
• If s.length() == 0, has it been initialized?
• Monads are difficult due to lack of functional support
• [](auto x) { return f(x); } instead of
x => f(x) as in C#
• No implicits (e.g. Kotlin’s ‘it’)
That’s It!
• Questions?
• Design Patterns in C++ courses on Pluralsight
• dmitrinеsteruk /at/ gmail.com
• @dnesteruk
Design Patterns in Modern
C++, Part II
Dmitri Nesteruk
dmitrinеstеruk@gmаil.соm
@dnesteruk
What’s In This Talk?
• Part II of my Design Patterns talks
• Part I of the talk available online inEnglish and Russian
• Examples of design patterns implemented in C++
• Disclaimer: C++ hasn’t internalized any patterns (yet)
• Patterns
• Memento
• Visitor
• Observer
• Interpreter
Memento
Helping implement undo/redo
Bank Account
• Bank account has a balance
• Balance changed via
• Withdrawals
• Deposits
• Want to be able to undo an erroneous
transaction
• Want to navigate to an arbitrary point
in the account’s changeset
class BankAccount
{
int balance{0};
public:
void deposit(int x) {
balance += x;
}
void withdraw(int x) {
if (balance >= x)
balance -= x;
}
};
Memento (a.k.a. Token, Cookie)
class Memento
{
int balance;
public:
Memento(int balance)
: balance(balance)
{
}
friend class BankAccount;
};
• Keeps the state of the balance at
a particular point in time
• State is typically private
• Can also keep reason for latest
change, amount, etc.
• Returned during bank account
changes
• Memento can be used to restore
object to a particular state
Deposit and Restore
Memento deposit(int amount)
{
balance += amount;
return { balance };
}
void restore(const Memento& m)
{
balance = m.balance;
}
BankAccount ba{ 100 };
auto m1 = ba.deposit(50); // 150
auto m2 = ba.deposit(25); // 175
// undo to m1
ba.restore(m1); // 150
// redo
ba.restore(m2); // 175
Storing Changes
class BankAccount {
int balance{0}, current;
vector<shared_ptr<Memento>> changes;
public:
BankAccount(const int balance) : balance(balance)
{
changes.emplace_back(make_shared<Memento>(balance));
current = 0;
}
};
Undo and Redo
shared_ptr<Memento> deposit(int
amount)
{
balance += amount;
auto m = make_shared<Memento>(
balance);
changes.push_back(m);
++current;
return m;
}
shared_ptr<Memento> undo()
{
if (current > 0)
{
--current;
auto m = changes[current];
balance = m->balance;
return m;
}
return{};
}
Undo/Redo Problems
• Storage excess
• Need to store only changes, but still…
• Need to be able to overwrite the Redo step
• Undo/redo is typically an aggregate operation
Cookie Monster!
• Why doesn’t push_back() return a
reference?
• Well, it could, but…
• As long as you’re not resizing due
to addition
• How to refer to an element of
vector<int>?
• Dangerous unless append-only
• Change to vector<shared_ptr<int>>
• Change to list<int>
• Some range-tracking magic token
solution
• Return a magic_cookie that
• Lets you access an element provided
it exists
• Is safe to use if element has been
deleted
• Keeps pointing to the correct element
even when container is reordered
• Requires tracking all mutating
operations
• Is it worth it?
Observer
This has been done to death, but…
Simple Model
class Person
{
int age;
public:
void set_age(int age)
{
this->age = age;
}
int get_age() const
{
return age;
}
};
• Person has an age: private field
with accessor/mutator
• We want to be informed
whenever age changes
• Need to modify the setter!
Person Listener
struct PersonListener
{
virtual ~PersonListener() = default;
virtual void person_changed(Person& p,
const string& property_name, const any new_value) = 0;
};
Person Implementation
class Person
{
vector<PersonListener*> listeners;
public:
void subscribe(PersonListener* pl) {
listeners.push_back(pl);
}
void notify(const string& property_name, const any new_value)
{
for (const auto listener : listeners)
listener->person_changed(*this, property_name, new_value);
}
};
Setter Change
void set_age(const int age)
{
if (this->age == age) return;
this->age = age;
notify("age", this->age);
}
Consumption
struct ConsoleListener : PersonListener
{
void person_changed(Person& p,
const string& property_name,
const any new_value) override
{
cout << "person's " << property_name
<< " has been changed to ";
if (property_name == "age")
{
cout << any_cast<int>(new_value);
}
cout << "n";
}
};
Person p{14};
ConsoleListener cl;
p.subscribe(&cl);
p.set_age(15);
p.set_age(16);
Dependent Property
bool get_can_vote() const
{
return age >= 16;
}
• Where to notify?
• How to detect that any
affecting property has
changed?
• Can we generalize?
Notifying on Dependent Property
void set_age(const int age)
{
if (this->age == age) return;
auto old_c_v = get_can_vote();
this->age = age;
notify("age", this->age);
auto new_c_v = get_can_vote();
if (old_c_v != new_c_v)
{
notify("can_vote", new_c_v);
}
}
 save old value
 get new value
 compare and notify only if
changed
Observer Problems
• Multiple subscriptions by a single listener
• Are they allowed? If not, use std::set
• Un-subscription
• Is it supported?
• Behavior if many subscriptions/one listener?
• Thread safety
• Reentrancy
Thread Safety
static mutex mtx;
class Person {
⋮
void subscribe(PersonListener* pl)
{
lock_guard<mutex> guard{mtx};
⋮
}
void unsubscribe(PersonListener* pl)
{
lock_guard<mutex> guard{mtx};
for (auto it : listeners)
{
if (*it == pl) *it = nullptr;
// erase-remove in notify()
}
}
};
• Anything that touches the list of subscribers
is locked
• Reader-writer locks better (shared_lock for
reading, unique_lock for writing)
• Unsubscription simply nulls the listener
• Must check for nullptr
• Remove at the end of notify()
• Alternative: use concurrent_vector
• Guaranteed thread-safe addition
• No easy removal
Reentrancy
struct TrafficAdministration : Observer<Person>
{
void field_changed(Person& source,
const string& field_name)
{
if (field_name == "age")
{
if (source.get_age() < 17)
cout << "Not old enough to drive!n";
else
{
// let's not monitor them anymore
cout << "We no longer care!n";
source.unsubscribe(this);
}}}};
Age changes (1617):
• notify() called
• Lock taken
field_changed
• unsubscribe()
unsubscribe()
• Tries to take a lock
• But it’s already taken 
Observer Problems
• Move from mutex to recursive_mutex
• Doesn’t solve all problems
• See Thread-safe Observer Pattern – You’re doing it Wrong (Tony Van
Eerd)
Boost.Signals2
• signal<T>
• A signal that can be sent to anyone willing
to listen
• T is the type of the slot function
• A slot is the function that receives the
signal
• Ordinary function
• Functor,std::function
• Lambda
• Connection
• signal<void()> s;
creates a signal
• auto c = s.connect([](){
cout << “test” << endl;
});
connects the signal to the slot
• More than one slot can be connected to a
signal
• Disconnection
• c.disconnect();
• Disconnects all slots
• Slots can be blocked
• Temporarily disabled
• Used to prevent infinite recursion
• shared_connection_block(c)
• Unblocked when block is destroyed, or
explicitly via
block.unblock();
INotifyPropertyChanged<T>
template <typename T>
struct INotifyPropertyChanged
{
virtual ~INotifyPropertyChanged() = default;
signal<void(T&, const string&)> property_changed;
};
struct Person : INotifyPropertyChanged<Person>
{
void set_age(const int age)
{
if (this->age == age) return;
this->age = age;
property_changed(*this, "age");
}
};
Consuming INPC Model
Person p{19};
p.property_changed.connect(
[](Person&, const string& prop_name)
{
cout << prop_name << " has been changed" << endl;
});
p.set_age(20);
Interpreter
Make your own programming language!
Interpreter
• Interpret textual input
• A branch of computer science
• Single item: atoi, lexical_cast, etc.
• Custom file format: XML, CSV
• Embedded DSL: regex
• Own programming language
Interpreting Numeric Expressions
(13-4)-(12+1)
Lex: [(] [13] [-] [4] [)] [-] …
Parse: Op(-, Op(-, 13, 4), Op(+,12,1))
Token
struct Token
{
enum Type {
integer, plus, minus,
lparen, rparen
} type;
string text;
explicit Token(Type type,
const string& text) :
type{type}, text{text} {}
};
Lexing
vector<Token> lex(const string& input)
{
vector<Token> result;
for (int i = 0; i < input.size(); ++i)
{
switch (input[i])
{
case '+':
result.push_back(Token{ Token::plus, "+" });
break;
case '-':
result.push_back(Token{ Token::minus, "-" });
break;
case '(':
result.push_back(Token{ Token::lparen, "(" });
break;
case ')':
result.push_back(Token{ Token::rparen, ")" });
break;
default:
ostringstream buffer;
buffer << input[i];
for (int j = i + 1; j < input.size(); ++j)
{
if (isdigit(input[j]))
{
buffer << input[j];
++i;
}
else
{
result.push_back(
Token{ Token::integer, buffer.str() });
break;
}
}
…
Parsing Structures
struct Element
{
virtual ~Element() = default;
virtual int eval() const = 0;
};
struct Integer : Element
{
int value;
explicit Integer(const int value)
: value(value)
{
}
int eval() const override {
return value;
}
};
struct BinaryOperation : Element
{
enum Type { addition, subtraction } type;
shared_ptr<Element> lhs, rhs;
int eval() const override
{
if (type == addition)
return lhs->eval() + rhs->eval();
return lhs->eval() - rhs->eval();
}
};
shared_ptr<Element> parse(const vector<Token>&
tokens)
{
⋮
}
Parsing Numeric Expressions
string input{ "(13-4)-(12+1)" };
auto tokens = lex(input);
try {
auto parsed = parse(tokens);
cout << input << " = " << parsed->eval() << endl;
}
catch (const exception& e)
{
cout << e.what() << endl;
}
Boost.Sprit
• A Boost library for interpreting text
• Uses a de facto DSL for defining the parser
• Defining a separate lexer not mandatory
• Favors Boost.Variant for polymorphic types
• Structurally matches definitions to OOP structures
Tlön Programming Language
• Proof-of-concept language transpiled into C++
• Parser + pretty printer +notepadlike app
• Some language features
• Shorter principal integral types
• Primary constructors
• Tuples
• Non-keyboard characters (keyboard-unfriendly)
Visitor
Adding Side Functionality to Classes
• C++ Committee not liking UCS
• Or any other “extension function” kind of deal
• Possible extensions on hierarchies end up being intrusive
• Need to modify the entire hierarchy
That’s It!
• Design Patterns in C++ courses on Pluralsight
• Leanpub book (work in progress!)
• Tlön Programming Language
• dmitrinеsteruk /at/ gmail.com
• @dnesteruk

Design Patterns in Modern C++

  • 1.
    Design Patterns in ModernC++, Part I Dmitri Nesteruk dmitrinеstеruk@gmаil.соm @dnesteruk
  • 2.
    What’s In ThisTalk? • Examples of patterns and approaches in OOP design • Adapter • Composite • Specification pattern/OCP • Fluent and Groovy-style builders • Maybe monad
  • 3.
  • 4.
    STL String Complaints •Making a string is easy string s{“hello world”}; • Getting its constituent parts is not vector<strings> words; boost::split(words, s, boost::is_any_of(“ “)); • Instead I would prefer auto parts = s.split(“ “); • It should work with “hello world” • Maybe some other goodies, e.g. • Hide size() • Have length() as a property, not a function
  • 5.
    Basic Adapter class String{ string s; public: String(const string &s) : s{ s } { } };
  • 6.
    Implement Split class String{ string s; public: String(const string &s) : s{ s } { } vector<string> split(string input) { vector<string> result; boost::split(result, s, boost::is_any_of(input), boost::token_compress_on); return result; } };
  • 7.
    Length Proxying class String{ string s; public: String(const string &s) : s{ s } { } vector<string> split(string input); size_t get_length() const { return s.length(); } };
  • 8.
    Length Proxying class String{ string s; public: String(const string &s) : s{ s } { } vector<string> split(string input); size_t get_length() const { return s.length(); } // non-standard! __declspec(property(get = get_length)) size_t length; };
  • 9.
    String Wrapper Usage Strings{ "hello world" }; cout << "string has " << s.length << " characters" << endl; auto words = s.split(" "); for (auto& word : words) cout << word << endl;
  • 10.
    Adapter Summary • Aggregateobjects (or keep a reference) • Can aggregate more than one • E.g., string and formatting • Replicate the APIs you want (e.g., length) • Miss out on the APIs you don’t need • Add your own features :)
  • 11.
  • 12.
    Scenario • Neurons connectto other neurons • Neuron layers are collections of neurons • These two need to be connectable
  • 13.
    Scenario struct Neuron { vector<Neuron*> in,out; unsigned int id; Neuron() { static int id = 1; this->id = id++; } }
  • 14.
    Scenario struct NeuronLayer :vector<Neuron> { NeuronLayer(int count) { while (count-- > 0) emplace_back(Neuron{}); } }
  • 15.
    State Space Explosition •void connect_to(Neuron& other) { out.push_back(&other); other.in.push_back(this); } • Unfortunately, we need 4 functions • Neuron to Neuron • Neuron to NeuronLayer • NeuronLayer to Neuron • NeuronLayer to NeuronLayer
  • 16.
    One Function Solution? •Simple: treat Neuron as NeuronLayer of size 1 • Not strictly correct • Does not take into account other concepts (e.g.,NeuronRing) • Better: expose a single Neuron in an iterable fashion • Other programming languages have interfaces for iteration • E.g., C# IEnumerable<T> • yield keyword • C++ does duck typing • Expects begin/end pair • One function solution not possible, but…
  • 17.
    Generic Connection Function structNeuron { ... template <typename T> void connect_to(T& other) { for (Neuron& to : other) connect_to(to); } template<> void connect_to<Neuron>(Neuron& other) { out.push_back(&other); other.in.push_back(this); } };
  • 18.
    Generic Connection Function structNeuronLayer : vector<Neuron> { … template <typename T> void connect_to(T& other) { for (Neuron& from : *this) for (Neuron& to : other) from.connect_to(to); } };
  • 19.
    How to Iterateon a Single Value? struct Neuron { … Neuron* begin() { return this; } Neuron* end() { return this + 1; } };
  • 20.
    API Usage Neuron n,n2; NeuronLayer nl, nl2; n.connect_to(n2); n.connect_to(nl); nl.connect_to(n); nl.connect_to(nl2);
  • 21.
  • 22.
    Open-Closed Principle • Openfor extension, closed for modification • Bad: jumping into old code to change a stable, functioning system • Good: making things generic enough to be externally extensible • Example: product filtering
  • 23.
    Scenario enum class Color{ Red, Green, Blue }; enum class Size { Small, Medium, Large }; struct Product { std::string name; Color color; Size size; };
  • 24.
    Filtering Products struct ProductFilter { typedefstd::vector<Product*> Items; static Items by_color(Items items, Color color) { Items result; for (auto& i : items) if (i->color == color) result.push_back(i); return result; } }
  • 25.
    Filtering Products struct ProductFilter { typedefstd::vector<Product*> Items; static Items by_color(Items items, Color color) { … } static Items by_size(Items items, Size size) { Items result; for (auto& i : items) if (i->size == size) result.push_back(i); return result; } }
  • 26.
    Filtering Products struct ProductFilter { typedefstd::vector<Product*> Items; static Items by_color(Items items, Color color) { … } static Items by_size(Items items, Size size) { … } static Items by_color_and_size(Items items, Size size, Color color) { Items result; for (auto& i : items) if (i->size == size && i->color == color) result.push_back(i); return result; } }
  • 27.
    Violating OCP • Keephaving to rewrite existing code • Assumes it is even possible (i.e. you have access to source code) • Not flexible enough (what about other criteria?) • Filtering by X or Y or X&Y requires 3 functions • More complexity -> state space explosion • Specification pattern to the rescue!
  • 28.
    ISpecification and IFilter template<typename T> struct ISpecification { virtual bool is_satisfied(T* item) = 0; }; template <typename T> struct IFilter { virtual std::vector<T*> filter( std::vector<T*> items, ISpecification<T>& spec) = 0; };
  • 29.
    A Better Filter structProductFilter : IFilter<Product> { typedef std::vector<Product*> Products; Products filter( Products items, ISpecification<Product>& spec) override { Products result; for (auto& p : items) if (spec.is_satisfied(p)) result.push_back(p); return result; } };
  • 30.
    Making Specifications struct ColorSpecification: ISpecification<Product> { Color color; explicit ColorSpecification(const Color color) : color{color} { } bool is_satisfied(Product* item) override { return item->color == color; } }; // same for SizeSpecification
  • 31.
    Improved Filter Usage Productapple{ "Apple", Color::Green, Size::Small }; Product tree { "Tree", Color::Green, Size::Large }; Product house{ "House", Color::Blue, Size::Large }; std::vector<Product*> all{ &apple, &tree, &house }; ProductFilter pf; ColorSpecification green(Color::Green); auto green_things = pf.filter(all, green); for (auto& x : green_things) std::cout << x->name << " is green" << std::endl;
  • 32.
    Filtering on 2..Ncriteria • How to filter by size and color? • We don’t want a SizeAndColorSpecification • State space explosion • Create combinators • A specification which combines two other specifications • E.g., AndSpecification
  • 33.
    AndSpecification Combinator template <typenameT> struct AndSpecification : ISpecification<T> { ISpecification<T>& first; ISpecification<T>& second; AndSpecification(ISpecification<T>& first, ISpecification<T>& second) : first{first}, second{second} { } bool is_satisfied(T* item) override { return first.is_satisfied(item) && second.is_satisfied(item); } };
  • 34.
    Filtering by SizeAND Color ProductFilter pf; ColorSpecification green(Color::Green); SizeSpecification big(Size::Large); AndSpecification<Product> green_and_big{ big, green }; auto big_green_things = pf.filter(all, green_and_big); for (auto& x : big_green_things) std::cout << x->name << " is big and green" << std::endl;
  • 35.
    Specification Summary • Simplefiltering solution is • Too difficult to maintain, violates OCP • Not flexible enough • Abstract away the specification interface • bool is_satisfied_by(T something) • Abstract away the idea of filtering • Input items + specification  set of filtered items • Create combinators (e.g., AndSpecification) for combining multiple specifications
  • 36.
  • 37.
    Scenario • Consider theconstruction of structured data • E.g., an HTML web page • Stuctured and formalized • Rules (e.g., P cannot contain another P) • Can we provide an API for building these?
  • 38.
    Building a SimpleHTML List // <ul><li>hello</li><li>world</li></ul> string words[] = { "hello", "world" }; ostringstream oss; oss << "<ul>"; for (auto w : words) oss << " <li>" << w << "</li>"; oss << "</ul>"; printf(oss.str().c_str());
  • 39.
    HtmlElement struct HtmlElement { string name; stringtext; vector<HtmlElement> elements; const size_t indent_size = 2; string str(int indent = 0) const; // pretty-print }
  • 40.
    Html Builder (non-fluent) structHtmlBuilder { HtmlElement root; HtmlBuilder(string root_name) { root.name = root_name; } void add_child(string child_name, string child_text) { HtmlElement e{ child_name, child_text }; root.elements.emplace_back(e); } string str() { return root.str(); } }
  • 41.
    Html Builder (non-fluent) HtmlBuilderbuilder{"ul"}; builder.add_child("li", "hello") builder.add_child("li", "world"); cout << builder.str() << endl;
  • 42.
    Making It Fluent structHtmlBuilder { HtmlElement root; HtmlBuilder(string root_name) { root.name = root_name; } HtmlBuilder& add_child(string child_name, string child_text) { HtmlElement e{ child_name, child_text }; root.elements.emplace_back(e); return *this; } string str() { return root.str(); } }
  • 43.
    Html Builder HtmlBuilder builder{"ul"}; builder.add_child("li","hello").add_child("li", "world"); cout << builder.str() << endl;
  • 44.
    Associate Builder &Object Being Built struct HtmlElement { static HtmlBuilder build(string root_name) { return HtmlBuilder{root_name}; } }; // usage: HtmlElement::build("ul") .add_child_2("li", "hello").add_child_2("li", "world");
  • 45.
    Groovy-Style Builders • Expressthe structure of the HTML in code • No visible function calls • UL { LI {“hello”}, LI {“world”} } • Possible in C++ using uniform initialization
  • 46.
    Tag (= HTMLElement) struct Tag { string name; string text; vector<Tag> children; vector<pair<string, string>> attributes; protected: Tag(const std::string& name, const std::string& text) : name{name}, text{text} { } Tag(const std::string& name, const std::vector<Tag>& children) : name{name}, children{children} { } }
  • 47.
    Paragraph struct P :Tag { explicit P(const std::string& text) : Tag{"p", text} { } P(std::initializer_list<Tag> children) : Tag("p", children) { } };
  • 48.
    Image struct IMG :Tag { explicit IMG(const std::string& url) : Tag{"img", ""} { attributes.emplace_back(make_pair("src", url)); } };
  • 49.
    Example Usage std::cout << P{ IMG {"http://pokemon.com/pikachu.png"} } << std::endl;
  • 50.
    Facet Builders • AnHTML element has different facets • Attributes, inner elements, CSS definitions, etc. • A Person class might have different facets • Address • Employment information • Thus, an object might necessitate several builders
  • 51.
    Personal/Work Information class Person { //address std::string street_address, post_code, city; // employment std::string company_name, position; int annual_income = 0; Person() {} // private! }
  • 52.
    Person Builder (ExposesFacet Builders) class PersonBuilder { Person p; protected: Person& person; explicit PersonBuilder(Person& person) : person{ person } { } public: PersonBuilder() : person{p} { } operator Person() { return std::move(person); } // builder facets PersonAddressBuilder lives(); PersonJobBuilder works(); }
  • 53.
    Person Builder (ExposesFacet Builders) class PersonBuilder { Person p; protected: Person& person; explicit PersonBuilder(Person& person) : person{ person } { } public: PersonBuilder() : person{p} { } operator Person() { return std::move(person); } // builder facets PersonAddressBuilder lives(); PersonJobBuilder works(); }
  • 54.
    Person Builder FacetFunctions PersonAddressBuilder PersonBuilder::lives() { return PersonAddressBuilder{ person }; } PersonJobBuilder PersonBuilder::works() { return PersonJobBuilder{ person }; }
  • 55.
    Person Address Builder classPersonAddressBuilder : public PersonBuilder { typedef PersonAddressBuilder Self; public: explicit PersonAddressBuilder(Person& person) : PersonBuilder{ person } { } Self& at(std::string street_address) { person.street_address = street_address; return *this; } Self& with_postcode(std::string post_code); Self& in(std::string city); };
  • 56.
    Person Job Builder classPersonJobBuilder : public PersonBuilder { typedef PersonJobBuilder Self; public: explicit PersonJobBuilder(Person& person) : PersonBuilder{ person } { } Self& at(std::string company_name); Self& as_a(std::string position); Self& earning(int annual_income); };
  • 57.
    Back to Person classPerson { // fields public: static PersonBuilder create(); friend class PersonBuilder; friend class PersonAddressBuilder; friend class PersonJobBuilder; };
  • 58.
    Final Person BuilderUsage Person p = Person::create() .lives().at("123 London Road") .with_postcode("SW1 1GB") .in("London") .works().at("PragmaSoft") .as_a("Consultant") .earning(10e6);
  • 59.
  • 60.
    Presence or Absence •Different ways of expressing absence of value • Default-initialized value • string s; // there is no ‘null string’ • Null value • Address* address; • Not-yet-initialized smart pointer • shared_ptr<Address> address • Idiomatic • boost::optional
  • 61.
    Monads • Design patternsin functional programming • First-class function support • Related concepts • Algebraic data types • Pattern matching • Implementable to some degree in C++ • Functional objects/lambdas
  • 62.
    Scenario struct Address { char* house_name;// why not string? } struct Person { Address* address; }
  • 63.
    Print House Name,If Any void print_house_name(Person* p) { if (p != nullptr && p->address != nullptr && p->address->house_name != nullptr) { cout << p->address->house_name << endl; } }
  • 64.
    Maybe Monad • Encapsulatethe ‘drill down’ aspect of code • Construct a Maybe<T> which keeps context • Context: pointer to evaluated element • person -> address -> name • While context is non-null, we drill down • If context is nullptr, propagation does not happen • All instrumented using lambdas
  • 65.
    Maybe<T> template <typename T> structMaybe { T* context; Maybe(T *context) : context(context) { } }; // but, given Person* p, we cannot make a ‘new Maybe(p)’ template <typename T> Maybe<T> maybe(T* context) { return Maybe<T>(context); }
  • 66.
    Usage So Far voidprint_house_name(Person* p) { maybe(p). // now drill down :) }
  • 67.
    Maybe::With template <typename T>struct Maybe { ... template <typename TFunc> auto With(TFunc evaluator) { if (context == nullptr) return ??? // cannot return maybe(nullptr) :( return maybe(evaluator(context)); }; }
  • 68.
    What is ??? •In case of failure, we need to return Maybe<U> • But the type of U should be the return type of evaluator • But evaluator returns U* and we need U • Therefore… • return Maybe< typename remove_pointer< decltype(evaluator(context)) >::type>(nullptr);
  • 69.
    Maybe::With Finished template <typenameTFunc> auto With(TFunc evaluator) { if (context == nullptr) return Maybe<typename remove_pointer< decltype(evaluator(context))>::type>(nullptr); return maybe(evaluator(context)); };
  • 70.
    Usage So Far voidprint_house_name(Person* p) { maybe(p) // now drill down :) .With([](auto x) { return x->address; }) .With([](auto x) { return x->house_name; }) . // print here (if context is not null) }
  • 71.
    Maybe::Do template <typename TFunc> autoDo(TFunc action) { // if context is OK, perform action on it if (context != nullptr) action(context); // no context transition, so... return *this; }
  • 72.
    How It Works •print_house_name(nullptr) • Context is null from the outset and continues to be null • Nothing happens in the entire evaluation chain • Person p; print_house_name(&p); • Context is Person, but since Address is null, it becomes null henceforth • Person p; p->Address = new Address; p->Address->HouseName = “My Castle”; print_house_name(&p); • Everything works and we get our printout
  • 73.
    Maybe Monad Summary •Example is not specific to nullptr • E.g., replace pointers with boost::optional • Default-initialized types are harder • If s.length() == 0, has it been initialized? • Monads are difficult due to lack of functional support • [](auto x) { return f(x); } instead of x => f(x) as in C# • No implicits (e.g. Kotlin’s ‘it’)
  • 74.
    That’s It! • Questions? •Design Patterns in C++ courses on Pluralsight • dmitrinеsteruk /at/ gmail.com • @dnesteruk
  • 75.
    Design Patterns inModern C++, Part II Dmitri Nesteruk dmitrinеstеruk@gmаil.соm @dnesteruk
  • 76.
    What’s In ThisTalk? • Part II of my Design Patterns talks • Part I of the talk available online inEnglish and Russian • Examples of design patterns implemented in C++ • Disclaimer: C++ hasn’t internalized any patterns (yet) • Patterns • Memento • Visitor • Observer • Interpreter
  • 77.
  • 78.
    Bank Account • Bankaccount has a balance • Balance changed via • Withdrawals • Deposits • Want to be able to undo an erroneous transaction • Want to navigate to an arbitrary point in the account’s changeset class BankAccount { int balance{0}; public: void deposit(int x) { balance += x; } void withdraw(int x) { if (balance >= x) balance -= x; } };
  • 79.
    Memento (a.k.a. Token,Cookie) class Memento { int balance; public: Memento(int balance) : balance(balance) { } friend class BankAccount; }; • Keeps the state of the balance at a particular point in time • State is typically private • Can also keep reason for latest change, amount, etc. • Returned during bank account changes • Memento can be used to restore object to a particular state
  • 80.
    Deposit and Restore Mementodeposit(int amount) { balance += amount; return { balance }; } void restore(const Memento& m) { balance = m.balance; } BankAccount ba{ 100 }; auto m1 = ba.deposit(50); // 150 auto m2 = ba.deposit(25); // 175 // undo to m1 ba.restore(m1); // 150 // redo ba.restore(m2); // 175
  • 81.
    Storing Changes class BankAccount{ int balance{0}, current; vector<shared_ptr<Memento>> changes; public: BankAccount(const int balance) : balance(balance) { changes.emplace_back(make_shared<Memento>(balance)); current = 0; } };
  • 82.
    Undo and Redo shared_ptr<Memento>deposit(int amount) { balance += amount; auto m = make_shared<Memento>( balance); changes.push_back(m); ++current; return m; } shared_ptr<Memento> undo() { if (current > 0) { --current; auto m = changes[current]; balance = m->balance; return m; } return{}; }
  • 83.
    Undo/Redo Problems • Storageexcess • Need to store only changes, but still… • Need to be able to overwrite the Redo step • Undo/redo is typically an aggregate operation
  • 84.
    Cookie Monster! • Whydoesn’t push_back() return a reference? • Well, it could, but… • As long as you’re not resizing due to addition • How to refer to an element of vector<int>? • Dangerous unless append-only • Change to vector<shared_ptr<int>> • Change to list<int> • Some range-tracking magic token solution • Return a magic_cookie that • Lets you access an element provided it exists • Is safe to use if element has been deleted • Keeps pointing to the correct element even when container is reordered • Requires tracking all mutating operations • Is it worth it?
  • 85.
    Observer This has beendone to death, but…
  • 86.
    Simple Model class Person { intage; public: void set_age(int age) { this->age = age; } int get_age() const { return age; } }; • Person has an age: private field with accessor/mutator • We want to be informed whenever age changes • Need to modify the setter!
  • 87.
    Person Listener struct PersonListener { virtual~PersonListener() = default; virtual void person_changed(Person& p, const string& property_name, const any new_value) = 0; };
  • 88.
    Person Implementation class Person { vector<PersonListener*>listeners; public: void subscribe(PersonListener* pl) { listeners.push_back(pl); } void notify(const string& property_name, const any new_value) { for (const auto listener : listeners) listener->person_changed(*this, property_name, new_value); } };
  • 89.
    Setter Change void set_age(constint age) { if (this->age == age) return; this->age = age; notify("age", this->age); }
  • 90.
    Consumption struct ConsoleListener :PersonListener { void person_changed(Person& p, const string& property_name, const any new_value) override { cout << "person's " << property_name << " has been changed to "; if (property_name == "age") { cout << any_cast<int>(new_value); } cout << "n"; } }; Person p{14}; ConsoleListener cl; p.subscribe(&cl); p.set_age(15); p.set_age(16);
  • 91.
    Dependent Property bool get_can_vote()const { return age >= 16; } • Where to notify? • How to detect that any affecting property has changed? • Can we generalize?
  • 92.
    Notifying on DependentProperty void set_age(const int age) { if (this->age == age) return; auto old_c_v = get_can_vote(); this->age = age; notify("age", this->age); auto new_c_v = get_can_vote(); if (old_c_v != new_c_v) { notify("can_vote", new_c_v); } }  save old value  get new value  compare and notify only if changed
  • 93.
    Observer Problems • Multiplesubscriptions by a single listener • Are they allowed? If not, use std::set • Un-subscription • Is it supported? • Behavior if many subscriptions/one listener? • Thread safety • Reentrancy
  • 94.
    Thread Safety static mutexmtx; class Person { ⋮ void subscribe(PersonListener* pl) { lock_guard<mutex> guard{mtx}; ⋮ } void unsubscribe(PersonListener* pl) { lock_guard<mutex> guard{mtx}; for (auto it : listeners) { if (*it == pl) *it = nullptr; // erase-remove in notify() } } }; • Anything that touches the list of subscribers is locked • Reader-writer locks better (shared_lock for reading, unique_lock for writing) • Unsubscription simply nulls the listener • Must check for nullptr • Remove at the end of notify() • Alternative: use concurrent_vector • Guaranteed thread-safe addition • No easy removal
  • 95.
    Reentrancy struct TrafficAdministration :Observer<Person> { void field_changed(Person& source, const string& field_name) { if (field_name == "age") { if (source.get_age() < 17) cout << "Not old enough to drive!n"; else { // let's not monitor them anymore cout << "We no longer care!n"; source.unsubscribe(this); }}}}; Age changes (1617): • notify() called • Lock taken field_changed • unsubscribe() unsubscribe() • Tries to take a lock • But it’s already taken 
  • 96.
    Observer Problems • Movefrom mutex to recursive_mutex • Doesn’t solve all problems • See Thread-safe Observer Pattern – You’re doing it Wrong (Tony Van Eerd)
  • 97.
    Boost.Signals2 • signal<T> • Asignal that can be sent to anyone willing to listen • T is the type of the slot function • A slot is the function that receives the signal • Ordinary function • Functor,std::function • Lambda • Connection • signal<void()> s; creates a signal • auto c = s.connect([](){ cout << “test” << endl; }); connects the signal to the slot • More than one slot can be connected to a signal • Disconnection • c.disconnect(); • Disconnects all slots • Slots can be blocked • Temporarily disabled • Used to prevent infinite recursion • shared_connection_block(c) • Unblocked when block is destroyed, or explicitly via block.unblock();
  • 98.
    INotifyPropertyChanged<T> template <typename T> structINotifyPropertyChanged { virtual ~INotifyPropertyChanged() = default; signal<void(T&, const string&)> property_changed; }; struct Person : INotifyPropertyChanged<Person> { void set_age(const int age) { if (this->age == age) return; this->age = age; property_changed(*this, "age"); } };
  • 99.
    Consuming INPC Model Personp{19}; p.property_changed.connect( [](Person&, const string& prop_name) { cout << prop_name << " has been changed" << endl; }); p.set_age(20);
  • 100.
    Interpreter Make your ownprogramming language!
  • 101.
    Interpreter • Interpret textualinput • A branch of computer science • Single item: atoi, lexical_cast, etc. • Custom file format: XML, CSV • Embedded DSL: regex • Own programming language
  • 102.
    Interpreting Numeric Expressions (13-4)-(12+1) Lex:[(] [13] [-] [4] [)] [-] … Parse: Op(-, Op(-, 13, 4), Op(+,12,1))
  • 103.
    Token struct Token { enum Type{ integer, plus, minus, lparen, rparen } type; string text; explicit Token(Type type, const string& text) : type{type}, text{text} {} };
  • 104.
    Lexing vector<Token> lex(const string&input) { vector<Token> result; for (int i = 0; i < input.size(); ++i) { switch (input[i]) { case '+': result.push_back(Token{ Token::plus, "+" }); break; case '-': result.push_back(Token{ Token::minus, "-" }); break; case '(': result.push_back(Token{ Token::lparen, "(" }); break; case ')': result.push_back(Token{ Token::rparen, ")" }); break; default: ostringstream buffer; buffer << input[i]; for (int j = i + 1; j < input.size(); ++j) { if (isdigit(input[j])) { buffer << input[j]; ++i; } else { result.push_back( Token{ Token::integer, buffer.str() }); break; } } …
  • 105.
    Parsing Structures struct Element { virtual~Element() = default; virtual int eval() const = 0; }; struct Integer : Element { int value; explicit Integer(const int value) : value(value) { } int eval() const override { return value; } }; struct BinaryOperation : Element { enum Type { addition, subtraction } type; shared_ptr<Element> lhs, rhs; int eval() const override { if (type == addition) return lhs->eval() + rhs->eval(); return lhs->eval() - rhs->eval(); } }; shared_ptr<Element> parse(const vector<Token>& tokens) { ⋮ }
  • 106.
    Parsing Numeric Expressions stringinput{ "(13-4)-(12+1)" }; auto tokens = lex(input); try { auto parsed = parse(tokens); cout << input << " = " << parsed->eval() << endl; } catch (const exception& e) { cout << e.what() << endl; }
  • 107.
    Boost.Sprit • A Boostlibrary for interpreting text • Uses a de facto DSL for defining the parser • Defining a separate lexer not mandatory • Favors Boost.Variant for polymorphic types • Structurally matches definitions to OOP structures
  • 108.
    Tlön Programming Language •Proof-of-concept language transpiled into C++ • Parser + pretty printer +notepadlike app • Some language features • Shorter principal integral types • Primary constructors • Tuples • Non-keyboard characters (keyboard-unfriendly)
  • 109.
  • 110.
    Adding Side Functionalityto Classes • C++ Committee not liking UCS • Or any other “extension function” kind of deal • Possible extensions on hierarchies end up being intrusive • Need to modify the entire hierarchy
  • 111.
    That’s It! • DesignPatterns in C++ courses on Pluralsight • Leanpub book (work in progress!) • Tlön Programming Language • dmitrinеsteruk /at/ gmail.com • @dnesteruk