This week I learned 19 — late post
Well. This week I thought about creating static interfaces in C++ with templates. Rust has this with generics and trait bounds. C# kind of has this, I think.
While it’s possible in C++, it’s cumbersome.
Let’s start with some ducks: a Duck
, a RoboDuck
, and a NotDuck
. Duck
and
RoboDuck
can Quack
, but NotDuck
can’t.
struct Duck
{
void Quack() const { std::cout << "Quack" << std::endl; }
};
struct RoboDuck
{
void Quack() const { std::cout << "Robotic quack" << std::endl; }
};
struct NotDuck {};
Instead of enforcing this with a pure virtual interface, we’ll create a series of templates that act as our traits.
namespace DuckTraits
{
template <typename T> struct Quacks : std::false_type {};
template <> struct Quacks<Duck> : std::true_type {};
template <> struct Quacks<RoboDuck> : std::true_type {};
template <typename T> struct DoesntQuack : std::true_type {};
template <> struct DoesntQuack<Duck> : std::false_type {};
template <> struct DoesntQuack<RoboDuck> : std::false_type {};
} /* DuckTraits */
Time to start quacking. We’ll define a free function that tells our ducks to quack.
namespace DuckOps
{
template <typename T>
void DoQuack(T &t, typename std::enable_if<DuckTraits::Quacks<T>::value>::type* = nullptr)
{
t.Quack();
}
template <typename T>
void DoQuack(T &t, typename std::enable_if<DuckTraits::DoesntQuack<T>::value>::type* = nullptr)
{
static_assert(DuckTraits::Quacks<T>::value, "Must be able to quack!");
}
} /* DuckOps */
These function templates will only allow things that can quack to compile. That is, this compiles:
% cat main.cpp
int main()
{
Duck d;
DuckOps::DoQuack(d);
RoboDuck rd;
DuckOps::DoQuack(rd);
}
% c++ -std=c++11 main.cpp
% ./a.out
Quack
Robotic quack
But this doesn’t:
% cat broken.cpp
int main()
{
NotDuck nd;
DuckOps::DoQuack(nd);
}
% c++ -std=c++11 broken.cpp
./DuckOps.h:18:2: error: static_assert failed due to requirement 'DuckTraits::Quacks<NotDuck>::value' "Must be able to quack!"
static_assert(DuckTraits::Quacks<T>::value, "Must be able to quack!");
^ ~~~~~~~~~~~~~~~~~~~~~~~~~~~~
broken.cpp:10:11: note: in instantiation of function template specialization 'DuckOps::DoQuack<NotDuck>' requested here
DuckOps::DoQuack(nd);
^
1 error generated.
make: *** [main.o] Error 1
Some closing thoughts
This is not fun. At least the way I implemented is not fun. It’s a lot of boilerplate. It forces you to be extremely explicit. You must enumerate all valid types for a given trait. But, of course, this comes with a benefit: static polymorphism akin to Rust’s trait bounds. C++20 aims to make this easier with constraints and concepts, but it still doesn’t seem as nice as Rust.