0

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:

  • device logic depends only on a communication protocol (messages).
  • connection_logic is just a source of byte arrays and may use different kinds of connections: SerialPort, tcp, udp, custom PCI express device, etc.

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:

  • Is it a legit approach?
  • Wouldn't it be confusing for a class provider?
  • Will it be (at what extent) harder to maintain?
  • What may be the differences in performance for compiling?
Sergey Kolesnik
  • 3,009
  • 1
  • 8
  • 28
  • What about, instead of testing a `writeable` tag in the static assert, testing for the actual availability of a write function with the expected signature? (and the same about reading of course) – prog-fh Feb 22 '21 at 08:47
  • @prog-fh I was thinking about it. But it is impossible if a write method is a template. `decltype(&T::write)` will fail since there needs to be an instantiation which is missing. Moreover it is just an example. Take for instance iterator traits... – Sergey Kolesnik Feb 22 '21 at 08:50
  • Isn't [`std::declval`](https://en.cppreference.com/w/cpp/utility/declval) dedicated to this case? – prog-fh Feb 22 '21 at 08:52
  • Dependency is introduced when you decide to make class satisfy some requirements from that library regardless of the way traits are implemented. "Another solution" is completely broken because it makes it impossible to distinguish between `writeable` added to satisfy library requirements and added for unrelated purpose. – user7860670 Feb 22 '21 at 08:54
  • @prog-fh I guess not. `std::declval` makes possible to deduce type when it is impossible to use a constructor. In case of a template method it is impossible to deduce its type. `operator()(RandomIter begin, RandomIter end)` is impossible to deduce since `RandomIter` is a template type. In case multiple instantiations happen it will be a compile time error because of ambiguity which `operator()(begi, end)` to use in `decltype`. – Sergey Kolesnik Feb 22 '21 at 08:57
  • @user7860670 dependency on specification is not the same as dependency on a header. In the second case you have to provide a header in some way. It may be frustrating: either you force others to copy it, which is not good for project management and CD, or you force yourself to provide packaging tools. "Added for unrelated purpose" is a general misuse of a specifications. It can happen with the first solution also. – Sergey Kolesnik Feb 22 '21 at 09:00
  • Not really, even with "dependency on specification" corresponding library must be made available at least to make writing some tests possible. Struggle with manual dependency management has nothing to do with it. With first solution "Added for unrelated purpose" may not happen because traits are implemented specifically for that library. – user7860670 Feb 22 '21 at 09:04
  • @user7860670 this is `you pay as you use (go)`. In the basic case you can make a repo with a class that provides basic logic and you only test it for implementation. Integration tests are only needed when you integrate your class. And those may be made in a completely different repo. In some way you just provide "forward declarations" for tags. It is a common practice to forward declare 3rdparty classes in headers. Why would this case be any different? – Sergey Kolesnik Feb 22 '21 at 09:08
  • @user7860670 why is it legit with template method/function signatures but not legit with tag types? When you define an `UnaryPredicate` you are not forced to use stl. But if you use stl, you are obliged to define an `UnaryPredicate`. And this `UnariPredicate` may be added for "completely unrelated purpose". So, what's the difference again? – Sergey Kolesnik Feb 22 '21 at 09:14

1 Answers1

2

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

standard uses different approaches:

  • presence of type such as transparent comparers which should have a "type" is_transparent (as using is_transparent = void;)

  • specific tags as iterator_tags.

  • or even just duck-typing (no check for template)

  • Or SFINAE on existence of method/properties.

Those types might be:

  • inside the class (as for is_transparent)
  • or provided as external traits such as std::iterator_traits (which even allows to extract, when possible, inner typedef from the class).

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

  • "physical" dependency, where stuff are, so, more linked, and possibly simpler to stay synchronized, but create a dependency.

  • no "physical" dependency, so potentially harder to stay synchronized.

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, ...)...

Jarod42
  • 203,559
  • 14
  • 181
  • 302
  • Yes. STL uses the approach when you are expected to provide `is_transparent` and the library deduces what kind of transparancy you provide. But STL templates in general are based on checking whether a class has defined a method (`operator()()` for example). So I thought what big difference is to check whether it has defined a type without checking "its "value" (as of `itaerator_tag`). – Sergey Kolesnik Feb 22 '21 at 10:52