I have come across the interesting and powerful concept of "traits" recently and am attempting to understand/implement them in C++. From what I understand, traits provide a way both extend/adapt the functionality of exiting code and define an "interface" for a class without using traditional inheritance (and all the overhead/problems that come with it). I also see that this concept seems to be closely related to the CRTP design pattern in C++.
As an example, my normal thought process for writing an interface in C++ would be to define a class with pure virtual methods. I can then create a subclass of this and pass a pointer to all my generic code. I am discovering that this has some problems however:
Here is an example of a simple, traditional interface for a Light:
class Light {
public:
virtual void on() = 0;
virtual void off() = 0;
};
class MyLight : public Light {
public:
void on() override;
void off() override;
};
void lightController(Light& l) {
l.on();
l.off();
}
And (based upon the article here: https://chrisbranch.co.uk/2015/02/make-your-c-interfaces-trait-forward/) here is what I think is a "traits-based" implementation of the same concept:
template<typename T>
class Light {
public:
Light(T& self) : _self(self) {}
void on() { _self.on(); }
void off() { _self.off(); }
private:
T& _self;
};
class MyLight {
public:
void on();
void off();
};
class OddLight {
public:
void set(bool state);
};
template<>
class Light<OddLight> {
public:
Light(OddLight& self) : _self(self) {}
void on() { _self.set(true); }
void off() { _self.set(false); }
private:
OddLight& _self;
};
template<typename T>
void lightUser1(T& l) {
Light<T> light(l);
light.on();
light.off();
}
template<typename T>
void lightUser2(Light<T>& l) {
light.on();
light.off();
}
I have a few questions about this:
Thank You!
That looks like an adapter, not a trait as is used in C++.
Traits in C++ is like std::numeric_limits
or std::iterator_traits
. It takes a type and returns some information about that type. The default implementation handles a certain number of cases, and you can specialize it to handle other cases.
The code he wrote has a few issues.
In Rust, this is used for dynamic dispatch. The template version is not dynamic.
C++ thrives on value types. With the embedded reference, this cannot be a value type.
The checking is late, at duck typing time, and the errors show up in the trait code, not at the call site.
An alternative approach to this is to use free functions and concepts and ADL.
turn_light_on(foo)
and turn_light_off(foo)
can be defaulted and found via ADL allowing customization of existing types. If you want to avoid the "one namespace" problem you can include an interface tag.
namespace Light {
struct light_tag{};
template<class T>
concept LightClass = requires(T& a) {
{ a.on() };
{ a.off() };
};
void on(light_tag, LightClass auto& light){ light.on(); }
void off(light_tag, LightClass auto& light){ light.off(); }
// also, a `bool` is a light, right?
void on(light_tag, bool& light){ light=true; }
void off(light_tag, bool& light){ light=false; }
template<class T>
concept Light = requires(T& a) {
{ on( light_tag{}, a ) };
{ off( light_tag{}, a ) };
};
void lightController(Light auto& l) {
on(light_tag{}, l);
off(light_tag{}, l);
}
struct SimpleLight {
bool bright = false;
void on() { bright = true; }
void off() { bright = false; }
};
}
then we have our OddLight
:
namespace Odd {
class OddLight {
public:
void set(bool state);
};
}
we want it to be a Light
, so we do this:
namespace Odd {
void on(::Light::light_tag, OddLight& odd){ odd.set(true); }
void off(::Light::light_tag, OddLight& odd){ odd.set(false); }
}
then
struct not_a_light{};
if we have test code:
int main() {
Light::SimpleLight simple;
Odd::OddLight odd;
not_a_light notLight;
Light::lightController(simple);
Light::lightController(odd);
// Light::lightController(notLight); // fails to compile, error is here
}
note that the concept map:
namespace Odd {
void on(::Light::light_tag, OddLight& odd){ odd.set(true); }
void off(::Light::light_tag, OddLight& odd){ odd.set(false); }
}
can be defined in either namespace Odd
or namespace Light
.
If you want to extend this to dynamic dispatch, you'd have to manually write the type erasure.
namespace Light {
struct PolyLightVtable {
void (*on)(void*) = nullptr;
void (*off)(void*) = nullptr;
template<Light T>
static constexpr PolyLightVtable make() {
using Light::on;
using Light::off;
return {
[](void* p){ on( light_tag{}, *static_cast<T*>(p) ); },
[](void* p){ off( light_tag{}, *static_cast<T*>(p) ); }
};
}
template<Light T>
static PolyLightVtable const* get() {
static constexpr auto retval = make<T>();
return &retval;
}
};
struct PolyLightRef {
PolyLightVtable const* vtable = 0;
void* state = 0;
void on() {
vtable->on(state);
}
void off() {
vtable->off(state);
}
template<Light T> requires (!std::is_same_v<std::decay_t<T>, PolyLightRef>)
PolyLightRef( T& l ):
vtable( PolyLightVtable::get<std::decay_t<T>>() ),
state(std::addressof(l))
{}
};
}
now we can write:
void foo( Light::PolyLightRef light ) {
light.on();
light.off();
}
and we get dynamic dispatch; the definition of foo
can be hidden from the caller.
Extending PolyLightRef
to PolyLightValue
isn't that tricky -- we just add in assign(move/copy)/construct(move/copy)/destroy to the vtable, and either stuff the state onto the heap in the void*
or use small buffer optimization in some cases.
And now we have a full rust-esque system of dynamic "trait" based dispatch, the traits are tested at the point of ingress (when you pass them either as Light auto
or PolyLightYYY
), customization in either the trait namespace or in the type's namespace, etc.
I, personally, am looking forward to c++23 having reflection, and the possibility of automating some of the above boilerplate.
There is actually a grid of useful variants:
RuntimePoly CompiletimePoly Concepts
PolyLightRef LightRef<T> Light&
PolyLightValue LightValue<T> Light
you can write to solve this problem in a Rust-like way.
c++17 deduction guides can be used to make the CompiletimePoly
a bit less annoying to use:
LightRef ref = light;
can deduce T
for you with
template<class T>
LightRef(T&)->LightRef<T>;
(this may be written for you), and at call site
LightRefTemplateTakingFunction( LightRef{foo} )