Trying to require multiple variadic types to be specific types

140 Views Asked by At

This is currently in pseudo code as it is an idea that I'm working on before I begin to write it as full code.

I know that I can create a normal variadic function that uses va_arg and va_list such as printf() does, however, I want to avoid using them completely.

I was thinking about using templated variadic parameters instead. I was thinking of making an instantiable class templated using variadic parameters. The condition here is that this class's constructor can only ever accept two types, but the number of either type can vary. I know that it is agnostic to where the parameters are written in code compared to how the compiler will interpret and the order in which they are called, but that's not an issue. The ordering I've chosen for the parameters is by convention for readability and consistency purposes.

Here's an example of the pseudo-code:

class TypeIn {...}
class TypeOut{...}

template<typename... T1, typename... T2>
class MyObject {
    std::array<TypeIn*> inputs_;
    std::array<TypeOut*> outputs_;
public:
    MyObject(T1&&... inputs, T2&& ... outputs) { ... }
};

Since I'm still working in C++17 and don't have C++20 with concepts, modules, and coroutines just yet, what would be the cleanest most reliable and efficient way to make sure that T1 is a TypeIn and T2 is a TypeOut class object and to populate the arrays accordingly? I could use vector, but once the object is constructed, the sizes of the inputs and outputs will not change.

Possible use case would be:

using In = TypeIn;
using Out = TypeOut;
MyObject obj( In a, In b, In c, Out x, Out y);

And I'd prefer not to have this for syntax if at all possible:

MyObject<In,In,In,Out,Out> obj( In a, In b, In c, Out X, Out y);

Since the first is cleaner or more readable.



Edit

After doing some thinking I was wondering if this might work instead...

class In {...}
class Out{...}


// template<typename T1 = In, typename T2 = Out>
// T1 must == type In and T2 must == type Out
class MyObject {
private:
     std::vector<In*> inputs_;
     std::vector<Out*> outputs_;

public:
      MyObject() = deafault;

      template<typename... Inputs> // would probably use move semantics or forwarding
      void assignInputs(Inputs&& ... inputs);

      template<typename... Outputs> // would probably use move semantics or forwarding
      void assignOutputs(Inputs&& ... outputs);
};

However, this would force the user to have to construct the object, then call both functions... I was trying to do all of this upon construction...

3

There are 3 best solutions below

1
On

After reading through the comments and the provided answer, and due to the nature of the language itself, it has come to my conclusion that since the number of types is fixed and known but their amounts are not that templates wouldn't even be required... It can just simply come down to a basic class with a simple constructor and move semantics, while using initialize_list.

class In{...};
class Out{...};

class MyObject {
private:
     std::vector<In> inputs_;
     std::vector<Out> outputs_;

public:
     MyObject(initializer_list<In> inputs, initializer_list<Out> outputs ) :
       inputs_( std::move(inputs) ),
       outputs_( std::move(outputs) )
     {}
};

Edit - I wasn't exactly trying to show code that will compile, it was more just to illustrate a point, however, I fixed it to match correctly for future readers.

0
On

I was trying to do all of this upon construction...

IMHO, the solution based on initializers list (see walnut's answer) is preferable, also to make clearer, from the caller point of view, which arguments are input values and which one are output values.

But... just for fun... if you really want to do all upon construction and avoiding grouping arguments (or also mixing they)... using std::tuple, std::tuple_cat(), delegating constructors, std::apply() and if constexpr

#include <iostream>
#include <type_traits>
#include <tuple>
#include <vector>

struct TypeIn  {};
struct TypeOut {};

struct MyObject
 {
   template <typename TargetType, typename T>
   static auto filterVal (T && t)
    {
      if constexpr ( true == std::is_same_v<TargetType, std::decay_t<T>> )
         return std::tuple<TargetType>{std::forward<T>(t)};
      else
         return std::tuple<>{};
    }

   std::vector<TypeIn> is;
   std::vector<TypeOut> os;

   template <typename ... It, typename ... Ot>
   MyObject (std::tuple<It...> && ti, std::tuple<Ot...> && to)
    : is{ std::apply([](auto && ... ts){
                        return std::vector<TypeIn>{
                           std::forward<decltype(ts)>(ts)... }; }, ti) },
      os{ std::apply([](auto && ... ts){
                        return std::vector<TypeOut>{
                           std::forward<decltype(ts)>(ts)... }; }, to) }
    { }

   template <typename ... Ts>
   MyObject (Ts && ... ts)
    : MyObject{std::tuple_cat( filterVal<TypeIn>(std::forward<Ts>(ts)) ... ),
               std::tuple_cat( filterVal<TypeOut>(std::forward<Ts>(ts)) ... )}
    { }
 };

int main ()
 {
   TypeIn   a, b, c, d;
   TypeOut  e, f;

   MyObject mo{ a, b, c, d, e, f };

   std::cout << mo.is.size() << " TypeIn vals\n"
             << mo.os.size() << " TypeOut vals\n";
 }

I repeat: just for fun.

8
On

According to your comments, I guess you are looking for something like

template<typename T1, typename T2>
class MyObject {
    std::vector<T1> inputs_;
    std::vector<T2> outputs_;
public:
    MyObject(std::initializer_list<T1> inputs, std::initializer_list<T2> outputs)
      : inputs_(inputs), outputs_(outputs) { }
};

To be used as

MyObject obj({a, b, c}, {x, y});

Note that this requires the two types to be copy-constructible. It does not work with move-only types.


If you really insist on taking the elements individually as constructor arguments, that is technically possible, but will be much more complex to implement with little benefit.