Understand the relationship between polymorphism and value semantics, learning some C++11 and 14 and showing off some small tools and techniques from Atria
The
diligent TDD developer The hardcore C++ developer (Hans said) (Ted cries) The modern C++ developer (Mona asked) (Ted replied) (Mona said) (Hans complained) (Hans complained) (Mona said) (Mona continued) (Ted interrupted) (Asked Hans) (Answered Ted) (Prompted Mona) (Answered Ted) (Hans replied with pride!) (Hans complained) (Hans claimed) (Replied Mona)Chapter 1
A testing story
Meet Ted
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
No problem!
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...
Meet Mona
What is your problem, Ted?
Oh, I'm so sad
...in a way that might make it slowerWhen all I wanted is
to use a different type in my test!
Exactly!
// 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!
// Feature.hpp
namespace detail {
template <class LicenseCheckerT>
struct Feature { ... };
}
using Feature = detail::Feature<LicenseChecker>;
No problem!
But now it compiles slower!
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!
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
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!
As you can see...
...virtual is virtually useless
Wait a second!
What if you want a container...
with objects of various types!
What types?
Let's say, tracks in a DAW!
Yeah, but which tracks?
Mmm, audio, midi, and
infinitely nested group
tracks!Easy peasy!
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!
Chapter 3
The type eraser
Wait a minute!
Sometimes you can't either...
You can't avoid inheritance here!
#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?
// 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...
Even if the stored type is only
copy-constructible
Storing small objects in the stack, sharing
instances of immutable models, ...
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
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