Search code examples
c++interfacetraits

Understanding C++ Traits and Making Them Efficient


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:

  1. Classes that need to inherit from multiple interfaces require the use of multiple inheritance which can become very complex and introduce the "diamond pattern" problem.
  2. A strict "is a" relationship is formed which is not always the intent. For example, if I am describing the interface to a Light, a Simulated Light is NOT really a Light, it merely has the "traits"/acts like a Light. What I mean is, the generic Light interface doesn't really have commonalities that an implementation needs to inherit, it merely defines how an implementation should behave.
  3. Virtual methods and inheritance allow full dynamic polymorphism which incurs unnecessary overhead. In most of my code, I would only ever be using a single implementation of an interface at a time and thus I don't need to dynamically choose the correct implementation, I just need to have the "users" of the interface be generic enough to all for different implementations.

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:

  1. Because, to use traits like this, you (temporarily) create a new Light instance, is there memory overhead associated with this?
  2. Is there a more effective method to document that a particular class "implements" a given trait?
  3. The article mentions two methods of defining a "user" for an interface. I have shown both above. lightUser2 seems to be the most well-document (it explicitly states that the function requires some implementation of the Light trait), however it requires that the implementations be explicitly casted into a Light outside of the function. Is there method to both document the intent of the user and all an implementation to be passed in directly?

Thank You!


Solution

  • 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.

    1. In Rust, this is used for dynamic dispatch. The template version is not dynamic.

    2. C++ thrives on value types. With the embedded reference, this cannot be a value type.

    3. 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 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.

    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} )
    

    Live example with error messages