Modernize your C++

Values, values, values.

Virtually never use virtual

a presention brought to you by
Juan Pedro
Bolivar Puente

@ Ableton



Goal

Understand the relationship between polymorphism and value semantics, learning some C++11 and 14 and showing off some small tools and techniques from Atria

github.com
/AbletonAG
/atria

Chapter 1

A testing story

Meet Ted

The diligent TDD developer

Ted has been assigned a story

As a Live suite user, I want to bla, bla, bla...

// tst_Feature.cpp
TEST(Feature, CantDoFeatureWithoutLicense)
{
  auto x = Feature {'not-a-suite-license'};
  EXPECT_FALSE(x.doStuff())
}
          

$ make check
COMPILER ERROR OMG
          

// Feature.hpp
struct Feature
{
  Feature (License x) {}

  bool doStuff() { return false; }
};
          

$ make check
ALL GREEN
          

// tst_Feature.cpp
TEST(Feature, DoFeatureWithLicense)
{
  auto x = Feature {'mock-suite-license'};
  EXPECT_TRUE(x.doStuff())
}
          

$ make check
FAILING TEST OMG
          

// Feature.hpp
struct Feature
{
  Feature (License x)
    : mLicense(std::move(license))
  {}
  bool doStuff();
private:
  License mLicense;
  LicenseChecker mChecker;
};
          

// Feature.cpp
bool Feature::doStuff() {
   return checker.check(mLicense, kLiveSuite);
}
          

Wait a minute!

How do I mock the license checker?









Meet Hans

The hardcore C++ developer

No problem!

(Hans said)

A virtual method here and there...

Some dependency passing...

Problem solved!


// ILicenseChecker.hpp
struct ILicenseChecker
{
  virtual check(License l, Product p) = 0;
};
          

// LicenseChecker.hpp
struct LicenseChecker : ILicenseChecker
{
  ...
};
          

struct Feature
{
  Feature (License license,
           std::unique_ptr<ILicenseChecker> pChecker =
             std::make_unique<LicenseChecker>())
    : mLicense(std::move(license))
    , mChecker(pChecker)
  {}
  ...
  std::unique_ptr<ILicenseChecker> mChecker;
          

bool Feature::doStuff() {
   return checker->check(mLicense, kLiveSuite);
}
          

// tst_Feature.cpp
struct MockLicenseChecker : ILicenseChecker {
  bool check(License l, Product) override {
    return l == 'suite-license';
  }
};

// ... in the tests ...
auto x = Feature {
  ...,
  std::make_unique<MockLicenseChecker>()
};
          

$ make check
ALL GREEN
          

All my tests are green but...

(Ted cries)

Meet Mona

The modern C++ developer

What is your problem, Ted?

(Mona asked)

Oh, I'm so sad

(Ted replied)

  • I had to change other's code
    ...in a way that might make it slower
  • I am now using the heap
  • I am now doing indirect calls
  • I now have to deal with object ownership

When all I wanted is

to use a different type in my test!

Exactly!

(Mona said)


// Feature.hpp
template <class LicenseCheckerT>
struct Feature
{
  Feature (License x)
    : mLicense(std::move(license)) {}

  bool doStuff() {
    return checker.check(mLicense, kLiveSuite);
  }

  LicenseCheckerT& checker() { return mChecker; }
private:
  License mLicense;
  LicenseCheckerT mChecker;
};
          

If you want to change a type, parametrize the type


// tst_Feature.cpp
struct MockLicenseChecker {
  bool check(License l, Product) {
    return l == 'suite-license';
  }
};

// ... in the tests ...
auto x = Feature<MockLicenseChecker> { ... };
          

$ make check
ALL GREEN
          

Hey, you changed the API!

(Hans complained)


// Feature.hpp
namespace detail {
  template <class LicenseCheckerT>
  struct Feature { ... };
}
using Feature = detail::Feature<LicenseChecker>;
          

No problem!

But now it compiles slower!

(Hans complained)

If you really care, no problem!


// Feature.hpp
extern template detail::Feature<LicenseChecker>;
          

// Feature.tpp
#include <Feature.hpp>
template <class T>
bool detail::Feature<T> check() { ... }
          

// Feature.cpp
#include <Feature.tpp>
template class detail::Feature<LicenseChecker>
          

And that's not all, look at this!

(Mona said)


struct MockLicenseChecker {
  std::function<bool(License, Product)> check =
    [this] (License l, Product) {
      return l == 'suite-license';
    }
};
          

TEST(Feature, CheckCalledOnlyOnce)
{
  using atria::testing;
  auto x = Feature {""};
  auto s = spy_on(x.checker().check);
  x.doStuff();
  EXPECT_EQ(1, s.count());
}
          

struct MockLicenseChecker {
  License mValidLicense = ""
  std::function<bool(License, Product)> check =
    [=] (License l, Product) {
      return l == this->mValidLicense;
    }
};
          

TEST(Feature, WorksWithValidLicense)
{
  auto x = Feature {"valid-license"};
  x.checker().mValidLicense = "valid-license";
  EXPECT_TRUE(x.doStuff());
}
          

Chapter 2

Types, which types?

So!

(Mona continued)

As you can see...

...virtual is virtually useless

Wait a second!

(Ted interrupted)

What if you want a container...

with objects of various types!

What types?

(Asked Hans)


Let's say, tracks in a DAW!

(Answered Ted)

Yeah, but which tracks?

(Prompted Mona)


Mmm, audio, midi, and
infinitely nested group tracks!

(Answered Ted)

Easy peasy!

(Hans replied with pride!)


You can't even copy those!

Well... yes you can!

Haha! Now implement recursive ungroup!

Didn't you get it already?

But that is going to blow up your interface!

Haven't you read the Gang of Four?!

Do you mean, their book on

abstract
expressionist
programming?

Back to modern times











Keep it simple!


struct MidiTrack {};
struct AudioTrack {};
template <typename TrackT>
using GroupTrack_ = std::vector<TrackT>;

using Track = typename boost::make_recursive_variant<
    MidiTrack,
    AudioTrack,
    GroupTrack_<boost::recursive_variant_>
  >::type;

using GroupTrack = GroupTrack_<Track>;
          

Adding operations is easy!


Track flattenTrack(Track track) {
  GroupTrack tracks;
  flattenTrack2(track, tracks);
  return tracks;
}

void flattenTrack2(Track track, GroupTrack& result) {
  using atria::variant::match;
  match(track,
    [&] (GroupTrack group) {
      for (auto& nested : group)
        flattenTrack2(nested, result);
    },
    [&] (MidiTrack midi)   { result.push_back(midi); },
    [&] (AudioTrack audio) { result.push_back(audio); });
}
          

And generic, if wanted...


Track flattenTrack(Track track) {
  GroupTrack tracks;
  flattenTrack2(track, tracks);
  return tracks;
}

void flattenTrack2(Track track, GroupTrack& result) {
  using atria::variant::match;
  match(track,
    [&] (GroupTrack group) {
      for (auto& nested : group)
        flattenTrack2(nested, result);
    },
    [&] (auto t) { result.push_back(t); });
}
          

Know your types, use variants!

  • No heap storage
  • No reference-semantics
  • Full type safety
  • Pattern-matching awesomeness!

Chapter 3

The type eraser

Wait a minute!

(Hans complained)

Sometimes you can't either...
  • Know all your types
  • Afford the compile time

You can't avoid inheritance here!

(Hans claimed)


#include <DrawLib.hpp>

struct MyView : drawlib::IDrawable {
  void draw() override {
    ...
  }
};

auto scene = drawlib::Scene {};
auto view = std::make_shared<MyView>();
scene.add(view);
          

How do you think these people did it?

(Replied Mona)


// No inheritance!
struct Myfunctor {
  void operator() { ++i; }
  int count = 0;
};

auto fn = std::function<void()> {}
auto functor = MyFunctor {}

// Value semantics by default!
fn = functor;
fn()
assert(functor.count == 0);
          

How do you think these people did it?


// Reference semantics choosen by clients!
auto fn = std::function<void()> {}
auto functor = MyFunctor {}

fn = std::ref(functor);
fn()
assert(functor.count == 1);
          

How do you think these people did it?


fn = std::function<int(int)> {};

// function pointer
int foo(int x);
std::function<void(int)>{ &foo };

// type only known to the compiler, and
// different signature of convertible types!
fn = [] (float x) { return 42.5; };

// binds returns type a crazy template!
fn = std::bind(std::plus<>{}, 42, _1);
          

We can do it too!


#include <BetterDrawLib.hpp>

// No inheritance + ADL support
struct MyView { ... };
void draw(const MyView& x);

// Third-party types, no wrappers
namespace std {
void draw(const std::vector<MyView>& x);
}

drawlib::Drawable v1 = std::vector<MyView> {},
                  v2 = MyView {};
auto s = drawlib::Scene {};
s.add(v1); s.add(v2);
          

namespace detail {

struct IDrawable {
  virtual void draw_() = 0;
  virtual IDrawable* clone_() const = 0;
};

} // namespace detail
          

Hans, calm down!

Virtual is actually legit
as an implementation detail


namespace detail {

template <typename T>
struct DrawableHolder : IDrawable {
  T value;
  template <typename U>
  DrawableHolder(U other)
    : value(std::move(other))
  {}
  void draw_() override {
    draw(this->value);
  }
  DrawableHolder* clone_() const override {
    return new DrawableHolder<T>(value);
  }
};

} // namespace detail
          

class Drawable {
  std::unique_ptr<detail::IDrawable> mHolder;
public:
  Drawable(Drawable&&) = default;
  Drawable& operator=(Drawable&&) = default;

  template <typename T>
  Drawable(T value)
    : mholder(new detail::DrawableHolder<T>(
        std::move(value))) {}

  Drawable(const Drawable& other)
    : mHolder(other.mHolder->clone_()) {}
  Drawable& operator=(const Drawable& other) {
    mHolder.reset(other.mHolder->clone_());
    return *this;
  }

  void draw() {
    mHolder->draw_();
  }
};
          

Well, yes, but...

  • Bigger library code, smaller client code
  • Copyable and moveable for free
    Even if the stored type is only copy-constructible
  • Oportunities for global optimization
    Storing small objects in the stack, sharing instances of immutable models, ...
  • We can do better
    adobe::poly, boost::type_erasure


BOOST_TYPE_ERASURE_FREE((drawlib)(has_draw), draw, 1);

namespace drawlib {

namespace tel = boost::type_erasure;
namespace mpl = boost::mpl;

using Drawable = tel::any<
  mpl::vector<
    tel::copy_constructible<>,
    has_draw<void(tel::_self)>,
    tel::relaxed
  > >;

} // namespace drawlib
          

Look Ma! No Boilerplate!

Moral

Being modern is about
not making compromises

  • Expresivity
  • Reusability
  • Safety
  • Performance

You can have it all!

Just remember

Give all your types to the compiler
By removing them from the program

Use auto, templates, deduced signatures, perfect forwarding, concepts...

Open questions

  • Documenting and checking concepts
  • Propagating mutation
  • Immutable data structures