Policy-based design in C++20

Meeting C++, Berlin 2018

Goran Aranđelović

goran.arandjelovic@gmail.com

Software

Software architecture

Programs

Algorithms

Data structures

Design patterns and idioms

Strategy pattern

Strategy pattern

Strategy pattern (example)

Strategy pattern (example)

Strategy pattern

struct device_lock
{
    virtual void lock() = 0;
    virtual void unlock() = 0;
};

struct no_lock : device_lock
{
    void lock() override {}
    void unlock() override {}
};

struct subscription_lock : device_lock
{
    void lock() override { /* ... */ }
    void unlock() override { /* ... */ }
};

Strategy pattern

struct player
{
    virtual void play_stream() = 0;
};

class hybrid_player : public player
{
    shared_ptr<stream_protection> sp_;
public:
    void set_stream_protection(
        shared_ptr<stream_protection> sp
    ) { sp_ = sp; }

    void play_stream() override {
        auto _dvb_stream = make_shared<dvb_stream>();
        auto _ip_stream = make_shared<ip_stream>();
        if (sp_->has_access(_dvb_stream) &&
            sp_->has_access(_ip_stream))
        {
            /* ... */
        }
    }
};

Strategy pattern

struct player
{
    virtual void play_stream() = 0;
};

class ip_player : public player
{
    shared_ptr<stream_protection> sp_;
public:
    void set_stream_protection(
        shared_ptr<stream_protection> sp
    ) { sp_ = sp; }

    void play_stream() override {
        auto _stream = make_shared<ip_stream>();
        if (sp_->has_access(_stream)) {
            /* ... */
        }
    }
};

Strategy pattern

struct stream {
    virtual ~stream() = default; // RTTI
};

struct stream_protection {
    virtual bool has_access(shared_ptr<stream>) = 0;
};

struct cas_protection : stream_protection {
    bool has_access(shared_ptr<stream>) override
    { /* ... */ }
};

struct drm_protection : stream_protection {
    bool has_access(shared_ptr<stream>) override
    { /* ... */ }
};

Strategy pattern

struct dvb_stream : stream { /* ... */ };
struct ip_stream : stream { /* ... */ };

class hybrid_protection : public stream_protection
{
    cas_protection cas_p{};
    drm_protection drm_p{};
public:
    bool has_access(shared_ptr<stream> s) override {
        if(auto dvb_s = dynamic_pointer_cast<dvb_stream>(s);
            dvb_s) {
            return cas_p.has_access(dvb_s);
        }
        else if(auto ip_s = dynamic_pointer_cast<ip_stream>(s);
            ip_s) {
            return drm_p.has_access(ip_s);
        }
        return false;
    }
};

Strategy pattern

struct dvb_stream : stream { /* ... */ };
struct ip_stream : stream { /* ... */ };

class hybrid_protection : public stream_protection
{
    cas_protection cas_p{};
    drm_protection drm_p{};
public:
    bool has_access(shared_ptr<stream> s) override {
        if(auto dvb_s = dynamic_pointer_cast<dvb_stream>(s);
            dvb_s) {
            return cas_p.has_access(dvb_s);
        }
        else if(auto ip_s = dynamic_pointer_cast<ip_stream>(s);
            ip_s) {
            return drm_p.has_access(ip_s);
        }
        return false;
    }
};
std::reinterpret_pointer_cast (since C++17)

Strategy pattern


        struct device
        {
            virtual void lock() = 0;
            virtual void unlock() = 0;
            virtual void watch() = 0;
        };

Strategy pattern

class cable_box : public device
{
    shared_ptr<player> player_;
    shared_ptr<device_lock> lock_;
public:
    /* other members omitted */

    void watch() override {
        /* ... */
        player_->play_stream();
    }

    void lock()   override { lock_->lock();   }
    void unlock() override { lock_->unlock(); }
};

Strategy pattern

void use_device(shared_ptr<device> d)
{
    d->unlock();
    d->watch();
    d->lock();
}

auto my_box = make_shared<cable_box>();
auto player = make_shared<hybrid_player>();
player->set_stream_protection(
    make_shared<hybrid_protection>()
);
my_box->set_player(player);

use_device(my_box);

"Designing software systems is hard because it constantly asks you to choose. And in program design, just as in life, choice is hard."

— Andrei Alexandrescu, Modern C++ design

https://salvarado24.wordpress.com/2012/10/17/the-road-to-monument-valley/

"Designing software systems is hard because it constantly asks you to choose. And in program design, just as in life, choice is hard."

— Andrei Alexandrescu, Modern C++ design

Stelvio pass (2757m), Eastern Alps - Italy

Templates in C++

template<typename T>
class list { /* ... */ };

int main()
{
    list<int> ints = {1, 2, 3, 4};
    list<string> strings = {"meeting", "c++"};
}

Templates in C++

template<int I>
struct factorial
{
    static constexpr int value = I * factorial<I-1>::value;
};

template<>
struct factorial<0>
{
    static constexpr int value = 1;
};

int main()
{
    cout << factorial<10>::value << '\n';
}

Templates in C++ (meta functions)

template<typename T>
struct add_pointer
{
    using type = T*;
}

template<typename T>
using add_pointer_t = typename add_pointer<T>::type;

int main()
{
    int i = 2018;
    
    add_pointer_t<int> p = &i;    // int *p = &i;
}
/* Modern C++ Design
   http://loki-lib.sourceforge.net
*/

template<typename T, typename U>
struct Typelist
{
    typedef T Head;
    typedef U Tail;
};

typedef Typelist<int, Typelist<bool, char>> my_types; 

Templates in C++

/* Modern C++ Design
   http://loki-lib.sourceforge.net
*/

template<typename T, typename U>
struct Typelist
{
    typedef T Head;
    typedef U Tail;
};

typedef Typelist<int, Typelist<bool, char> > my_types; 

Templates in C++

< <> >-programming

<<>>-programming

// Variadic templates

template<typename ...Ts>
struct Typelist {};

using my_types = Typelist<int, bool, char>;

Templates in C++

Loki

boost::mpl

boost::fusion

boost::hana

C++ Template Metaprogramming: Concepts, Tools, and Techniques from Boost and Beyond - David Abrahams, Aleksey Gurtovoy

Modern C++ Design - Andrei Alexandrescu

Joel de Guzman, Dan Marsden, Tobias Schwinger

Louis Dionne

template<typename T>                       // boost::hana - L.Dionne
struct type {};

template<typename T>
constexpr type<T*> add_pointer(type<T>)
{
    return {};
}

template<typename T, typename U>
constexpr std::false_type operator==(type<T>, type<U>)
{
    return {};
}

template<typename T>
constexpr std::true_type operator==(type<T>, type<T>)
{
    return {};
}

Templates in C++ (meta functions)



using int_ptr = add_pointer_t<int>;
static_assert(std::is_same<int_ptr, int*>{});

Templates in C++ (meta functions)



constexpr auto int_ptr = add_pointer(type<int>{});
static_assert(int_ptr == type<int*>{});

What is policy-based design?

"Policy-based design, also known as policy-based class design or policy-based programming, is a computer programming paradigm based on an idiom for C++ known as policies. It has been described as a compile-time variant of the strategy pattern, and has connections with C++ template metaprogramming."

wikipedia.org/wiki/Policy-based_design

Compile-time strategy pattern

Using policies to represent different (and orthogonal) behaviours

Solving combinatorial explosion (sort of...)

Policy-based design is...

Policies

struct Point { int x; int y; };

template<typename TablePolicy, typename RulesPolicy>
struct TableGame : private TablePolicy
{
    template<typename Player>
    void move_player(Player const &player, Point const &point)
    {
        if(RulesPolicy::valid_move(player, point))
        {
            move(player, point);
        }
    }
};

Policies

struct device_lock
{
    virtual void lock() = 0;
    virtual void unlock() = 0;
};

struct no_lock : device_lock
{
    void lock() override {}
    void unlock() override {}
};

struct subscription_lock : device_lock
{
    void lock() override { /* ... */ }
    void unlock() override { /* ... */ }
};
struct no_lock
{
    void lock() {}
    void unlock() {}
};

struct subscription_lock
{
    void lock() { /* ... */ }
    void unlock() { /* ... */ }
};

Policies

struct player
{
    virtual void play_stream() = 0;
};

class ip_player : public player
{
    shared_ptr<stream_protection> sp_;
public:
    void set_stream_protection(
        shared_ptr<stream_protection> sp
    ) { sp_ = sp; }

    void play_stream() override {
        auto _stream = make_shared<ip_stream>();
        if (sp_->has_access(_stream)) {
            /* ... */
        }
    }
};
template<typename StreamProtection>
struct ip_player 
{
    using SP = StreamProtection;

    void play_stream() {
        ip_stream _stream{};
        if (SP::has_access(_stream)) {
            /* ... */
        }
    }
};

Policies

struct player
{
    virtual void play_stream() = 0;
};

class hybrid_player : public player
{
    shared_ptr<stream_protection> sp_;
public:
    void set_stream_protection(
        shared_ptr<stream_protection> sp
    ) { sp_ = sp; }

    void play_stream() override {
        auto _dvb_stream = make_shared<dvb_stream>();
        auto _ip_stream = make_shared<ip_stream>();
        if (sp_->has_access(_dvb_stream) &&
            sp_->has_access(_ip_stream))
        {
            /* ... */
        }
    }
};
template<typename StreamProtection>
struct hybrid_player
{
    using SP = StreamProtection;

    void play_stream() {
        dvb_stream _dvb_stream{};
        ip_stream _ip_stream{};
        if (SP::has_access(_dvb_stream) &&
            SP::has_access(_ip_stream))
        {
            /* ... */
        }
    }
};

Policies

struct stream {
    virtual ~stream() = default; // RTTI
};

struct stream_protection {
    virtual bool has_access(shared_ptr<stream>) = 0;
};

struct cas_protection : stream_protection {
    bool has_access(shared_ptr<stream>) override
    { /* ... */ }
};

struct drm_protection : stream_protection {
    bool has_access(shared_ptr<stream>) override
    { /* ... */ }
};
struct dvb_stream { /* ... */ };
struct ip_stream { /* ... */ };

struct cas_protection {
    static bool has_access(dvb_stream)
    { /* ... */ }
};

struct drm_protection {
    static bool has_access(ip_stream)
    { /* ... */ }
};

Policies

struct dvb_stream : stream { /* ... */ };
struct ip_stream : stream { /* ... */ };

class hybrid_protection : public stream_protection
{
    cas_protection cas_p{};
    drm_protection drm_p{};
public:
    bool has_access(shared_ptr<stream> s) override {
        if(auto dvb_s = dynamic_pointer_cast<dvb_stream>(s);
            dvb_s) {
            return cas_p.has_access(dvb_s);
        }
        else if(auto ip_s = dynamic_pointer_cast<ip_stream>(s);
            ip_s) {
            return drm_p.has_access(ip_s);
        }
        return false;
    }
};
struct dvb_stream { /* ... */ };
struct ip_stream { /* ... */ };

struct hybrid_protection
    : cas_protection, drm_protection {
    using cas_protection::has_access;
    using drm_protection::has_access;
};

Policies

class cable_box : public device
{
    shared_ptr<player> player_;
    shared_ptr<device_lock> lock_;
public:
    /* other members omitted */

    void watch() override {
        /* ... */
        player_->play_stream();
    }

    void lock()   override { lock_->lock();   }
    void unlock() override { lock_->unlock(); }
};
template<typename LockPolicy,
         template<typename> typename Player,
         typename StreamProtection>
class cable_box : public LockPolicy
{
    Player<StreamProtection> player_;
public:
    /* other members omitted */

    void watch() {
        /* ... */
        player_.play_stream();
    }

    void print_lock_state() {
        cout << this->is_locked() << '\n';
    }
};

Strategies vs. Policies

void use_device(shared_ptr<device> d)
{
    d->unlock();
    d->watch();
    d->lock();
}

auto my_box = make_shared<cable_box>();
auto player = make_shared<hybrid_player>();
player->set_stream_protection(
    make_shared<hybrid_protection>()
);
my_box->set_player(player);

use_device(my_box);
template<typename Device>
void use_device(Device &d) {
    d.unlock();
    d.watch();
    d.lock();
}

cable_box<
    no_lock, hybrid_player, hybrid_protection
> my_box{};

use_device(my_box);

// my_box.print_lock_state(); ??

Detection idiom

Detection idiom

More elegant way of doing SFINAE

Higher-order meta function

Fills the gap between SFINAE and Concepts

Invented by Walter E. Brown

wg21.link/n4502

(Expression) SFINAE

template<typename T>    // #1
void f(typename T::value_type)
{}

template<typename T>    // #2
void f(T)
{}

struct X {
    using value_type = int;
};

f<X>(0);        // #1
f<int>(0);      // #2

(Expression) SFINAE

template<typename T>    // #1
void f(T, std::enable_if_t<std::is_pointer_v<T>>* = 0)
{}

template<typename T>    // #2
void f(T, std::enable_if_t<not std::is_pointer_v<T>>* = 0)
{}

int* p{};
int  i{};

f(p);    // #1
f(i);    // #2

(Expression) SFINAE

template<typename T>    // #1
auto f(T t) -> decltype(t.a(), void())
{}

template<typename T>    // #2
auto f(T t) -> decltype(t.b(), void())
{}

struct A { void a() {} };
struct B { void b() {} };
struct C {};

f(A{});    // #1
f(B{});    // #2
f(C{});    // error

std::void_t




            template<typename...>
            using void_t = void;

Detection idiom

template<typename Default,
         template<typename...> typename Op,
         typename,
         typename ...Args>
struct detector
{
    using value_t = std::false_type;
    using type = Default;
};

template<typename Default,
         template<typename...> typename Op,
         typename ...Args>
struct detector<Default, Op, std::void_t<Op<Args...>>, Args...>
{
    using value_t = std::true_type;
    using type = Op<Args...>;
};

Detection idiom

template<template<typename...> typename Op, typename ...Args>
using is_detected_t = typename detector<nonesuch, Op, std::void_t<>, Args...>::type;

template<template<typename...> typename Op, typename ...Args>
using is_detected = typename detector<nonesuch, Op, std::void_t<>, Args...>::value_t;

template<template<typename...> typename Op, typename ...Args>
constexpr bool is_detected_v = is_detected<Op, Args...>::value;

Detection idiom

template<typename T>
using drm_protection_t =
    decltype(T::has_access(std::declval<ip_stream>()));

template<typename T>
using has_drm_protection =
    is_detected<drm_protection_t, T>;
template<typename StreamProtection>
struct ip_player
{
    using SP = StreamProtection;

    static_assert(
        has_drm_protection<SP>{}
    );

    void play_stream() {
        ip_stream _stream{};
        if (SP::has_access(_stream)) {
            /* ... */
        }
    }
};

Detection idiom

template<typename T>
using cas_protection_t =
    decltype(T::has_access(std::declval<dvb_stream>()));

template<typename T>
using has_cas_protection =
    is_detected<cas_protection_t, T>;

template<typename T>
using has_hybrid_protection = std::conjunction<
    has_cas_protection<T>,
    has_drm_protection<T>
>;
template<typename StreamProtection>
struct hybrid_player
{
    using SP = StreamProtection;

    static_assert(
        has_hybrid_protection<SP>{}
    );

    void play_stream() {
        dvb_stream _dvb_stream{};
        ip_stream _ip_stream{};
        if (SP::has_access(_dvb_stream) &&
            SP::has_access(_ip_stream))
        {
            /* ... */
        }
    }
};

Detection idiom

template<typename T>
using lock_unlock_t = decltype(
    std::declval<T>().lock(),
    std::declval<T>().unlock()
);

template<typename T>
using play_stream_t
    = decltype(std::declval<T>().play_stream());

template<typename T>
using has_lock_unlock = is_detected<lock_unlock_t, T>;

template<typename T>
using has_play_stream = is_detected<play_stream_t, T>;
template<typename LockPolicy,
         template<typename> typename Player,
         typename StreamProtection>
class cable_box : public LockPolicy
{
    static_assert(
        has_lock_unlock<LockPolicy>{} &&
        has_play_stream<Player<StreamProtection>>{} 
    );

    Player<StreamProtection> player_;
public:
    /* other members omitted */

    void watch() {
        /* ... */
        player_.play_stream();
    }

    void print_lock_state() {
        cout << this->is_locked() << '\n';
    }
};

Concepts TS

Concepts

Named boolean predicates on template parameters

Constraints on class/function templates

Let us define interfaces in a different manner

template<typename T>           // "Dirty" concept
concept Doable = requires(T t) {
    { t.do_it() };
};

Concepts

template<typename T>
concept Iterable = requires(T range) {
    { begin(range) } -> Iterator_type<T>;
    { end(range) } -> Sentinel_type<T>;
    requires Iterator<Iterator_type<T>>;
    requires EqualityComparable<
        Iterator_type<T>, Sentinel_type<T>>;
};

Concepts

Example borrowed from: http://ericniebler.com/2014/02/21/introducing-iterables/

template<typename T>
void f(T const &t) requires Iterable<T>
{
    /* ... */
}

template<typename T>
void f(T const &t) requires not Iterable<T>
{
    /* ... */
}

Concepts

void f(C &c)        // What's the type of 'c'?
{
    /* ... */
}

void f(C &&c)
{
    /* ... */
}

Concepts

Concepts

template<typename T>
concept DrmProtectable = requires(T t) {
    { T::has_access(std::declval<ip_stream>()) } -> bool;
};
template<DrmProtectable StreamProtection>
struct ip_player
{
    using SP = StreamProtection;

    void play_stream() {
        ip_stream _stream{};
        if (SP::has_access(_stream)) {
            /* ... */
        }
    }
};

Concepts

template<typename T>
concept CasProtectable = requires(T t) {
    { T::has_access(std::declval<dvb_stream>()) } -> bool;
};

template<typename T>
concept HybridProtectable =
    CasProtectable<T> && DrmProtectable<T>;
template<HybridProtectable StreamProtection>
struct hybrid_player
{
    using SP = StreamProtection;

    void play_stream() {
        dvb_stream _dvb_stream{};
        ip_stream _ip_stream{};
        if (SP::has_access(_dvb_stream) &&
            SP::has_access(_ip_stream))
        {
            /* ... */
        }
    }
};

Concepts

template<typename T>
concept Lockable = requires(T t) {
    { t.lock() };
    { t.unlock() };
};

template<typename T>
concept Playable = requires(T t) {
    { t.play_stream() };
};
template<Lockable LockPolicy,
         template<typename> typename Player,
         typename StreamProtection> 
requires Playable<Player<StreamProtection>>
class cable_box : public LockPolicy
{
    Player<StreamProtection> player_;
public:
    /* other members omitted */

    void watch() {
        /* ... */
        player_.play_stream();
    }

    void print_lock_state() {
        cout << this->is_locked() << '\n';
    }
};

Concepts

template<typename T>
concept Device = requires(T t) {
    requires Lockable<T>;
    { t.watch() };
};
void use_device(Device auto &d) {  // wg21.link/p1141r1
    d.unlock();
    d.watch();
    d.lock();
}

cable_box<
    no_lock, hybrid_player, hybrid_protection
> my_box{};

use_device(my_box);

Are we good to go?

void my_func(cable_box<???, ???, ???> &device)
{
    /* ... */
}

Burn API, burn!

template<typename A, typename B, typename C>
void my_func(cable_box<A, B, C> &device)
{
    /* ... */
}

Burn API, burn!

void my_func(Device auto &device)
{
    /* ... */
}

Burn API, burn!

Type erasure

std::void_t

void*

A a{};

void *v = &a;

Type erasure

struct A {};
struct B : A {};

B *b = ... ;

A *a = b;

Type erasure

struct A {};
struct B : A {};

shared_ptr<B> b = ... ;

shared_ptr<A> a = b;

Type erasure

template<typename T>
struct shared_ptr
{
    template<typename U>
    shared_ptr(shared_ptr<U> const &,
        std::enable_if_t<std::is_convertible_v<U*, T*>>* = 0)
    { /* ... */ }
};

Type erasure

template<typename Player,
         typename StreamProtection>
void my_func(cable_box<any_lock, Player, StreamProtection> &device)
{
    /* ... */
}

cable_box<
    no_lock, hybrid_player, hybrid_protection
> my_box{};

my_func(my_box);

Type erasure

Sean Parent: Value semantics and concepts-based polymorphism

youtu.be/_BpMYeUFXv8

Thank you!

Policy-based design in C++20

By Goran Aranđelović

Private

Policy-based design in C++20