c++genericstemplate-meta-programmingtype-traitspolicy-based-design

Zero-dependency traits definition


I am experimenting and trying to make a template policy-based meta library. Example case is aggregating 2 classes for a device driver. The classes implement device_logic and connection_logic, they don't need to depend on each other's type:

The goal is not to force any interfaces or types on them. They must depend purely on the API specification and only provide necessary traits.

The STL approach is to define traits in a header and then use them inside a class. So the traits tags must be defined in a header of a template library.

// device_traits.h

namespace traits
{

   // tags to be defined as io_type
   struct writeable;
   struct readable;
   struct wretableReadable;


   template <typename T>
   constexpr bool is_writeable()
   {
       return std::is_same_v<writeable, typename T::io_type>() ||
              std::is_same_v<wretableReadable, typename T::io_type>();
   }

   // functions for readable and readableWriteable
      
}

template <typename ConnectionLogic,
          typename DeviceLogic>
class aggregate_device
{

static_assert(!traits::readable<DeviceLogic>() ||
              (traits::readable<DeviceLogic>() &&
               traits::readable<ConnectionLogic>()),
               "Device logic is readable so must be ConnectionLogic");

static_assert(!traits::writeable<DeviceLogic>() ||
              (traits::writeable<DeviceLogic>() &&
               traits::writeable<ConnectionLogic>()),
               "Device logic is writeable so must be ConnectionLogic");

};

In this case aggregate_device aggregates connection and device logic. If device logic is readable, the connection logic must provide input. If device logic is writeable, the connection must provide output.

// device_logic.h
#include <device_traits>

class device_logic
{
public:
   using io_type = traits::readableWriteable;
   // ... methdos, etc
};

This version works but introduces a dependency on the template library. Introducing dependency (even a header-only library) is not convenient for a developer and generally not good for a library. Someone might want to use device_logic class in another module or project, but not want to pull a template library it depends on.

Another solution which removes the dependency is not to force a class provider to inject io_type tags to his class but to define them on his own.

// device_traits.h

namespace traits
{

   template<typename, typename = void>
   struct is_writeable : std::false_type{};

   // here we just check if a typename has a type writeable
   template<typename T>
   struct is_writeable<T, std::void_t<typename T::writeable>> : std::true_type{};

   // functions for readable and readableWriteable
      
   // aggregator class
}

// device_logic.h
// don't include nothing


class device_logic
{
   public:

   // define a type 
   struct writeable;
};


/////
#include <device_traits>

static_assert(traits::is_writeable<device_logic>(), "");

Now I use the second approach and it works. The questions are:


Solution

  • Is it a legit approach?
    Wouldn't it be confusing for a class provider?

    standard uses different approaches:

    Those types might be:

    Notice that only external traits might support built-in types (pointers, int, ...) or external types (3rd library, or standard library for your traits) in a non-intrusive way.

    Will it be (at what extent) harder to maintain?

    There is a trade-of between

    What may be the differences in performance for compiling?

    As always, you have to measure.

    For example with build-bench.com.

    To use together, it seems you have to include similar code, but not necessary in same order, so I would bet for similar performance.

    When used independently, you should avoid one extra #include (so depends of it size/number of #include, if pch is used, ...)...