0

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

Community
  • 1
  • 1
Francis Cugler
  • 7,788
  • 2
  • 28
  • 59
  • There are many possible approaches, based on SFINAE or static_asserts. Which approach will work better for you? – Sam Varshavchik Mar 02 '20 at 13:20
  • @SamVarshavchik I understand that, but this is kind of where I'm stuck... at the decision tree, so some examples of different ways of doing this would be nice to see, then I can base my decision which route to take from there... – Francis Cugler Mar 02 '20 at 13:24
  • Do you really want your constructor to take rvalue references only? (forwarding references only happen when the function itself is a template) – NathanOliver Mar 02 '20 at 13:24
  • You edited your question. No matter what, I do not believe that your desired use case will be possible due to the nature of variadic template expansions. The first `...` will always consume all parameters. C++ will not try to find every combination of two separate parameter packs that match the template parameters. You can only deduce a maximum of one parameter pack, and it must be the last parameter. You will have to adjust your expectations, and then maybe someone will help you. – Sam Varshavchik Mar 02 '20 at 13:24
  • @Nathin well they don't have to bee RValue. They might be just pointers to objects... and at this point, I'm not sure if I want them to be a shared resource or a unique object. I'm leaning for the `InTypes` to be transferred via unique and for the `OutTypes` to be shared. – Francis Cugler Mar 02 '20 at 13:26
  • @SamVarshavchik I was leaning for creating an object that could have multiple amounts of inputs, and outputs but those two types will never change. I don't really want to be creating vectors or arrays of them and passing them around, although I could... I just want the class to construct them when it is being constructed. – Francis Cugler Mar 02 '20 at 13:28
  • 1
    What's wrong with `MyObject(std::array inputs, std::array outputs)`? – Jarod42 Mar 02 '20 at 13:29
  • @Jarod42 because the number of inputs and outputs can vary from instance to instance and isn't always known... – Francis Cugler Mar 02 '20 at 13:30
  • @FrancisCugler You can make both `N1` and `N2` templates parameters. At runtime, the number cannot vary between instances in your example anyway, because you are already using `std::array` in the example and you need to specify the number and types of function arguments at compile-time. – walnut Mar 02 '20 at 13:31
  • @walnut, N1 and N2 won't always be known at compile time. – Francis Cugler Mar 02 '20 at 13:33
  • @FrancisCugler Then the premise of your question is flawed. The number of arguments to a function can never depend on runtime properties. – walnut Mar 02 '20 at 13:34
  • Can we reduce this setting to split a tuple? –  Mar 02 '20 at 13:34
  • @FrancisCugler If that's the case you'll have to use a `std::vector`. Templates only work at compile time so a tuple or parameter pack must know the size at compile time. – NathanOliver Mar 02 '20 at 13:35
  • The template arguments should be only `` or am I wrong? –  Mar 02 '20 at 13:35
  • I was thinking similar to that of variadic functions that use `va_args`, `va_list` such as your `printf()` functions... but wanted to use template types instead... For example one instance of the class would be instantiated as: `MyObject obj1(in a, in b, out x);` where the next call would be `MyObject obj2(in a, in b, in c, out x, out y);` – Francis Cugler Mar 02 '20 at 13:36
  • 1
    @FrancisCugler That would still be at compile-time. You will need to save the elements in `std::vectors` though. Aside from changing `std::array` to `std::vector`, the suggestion from Jarod42 will still work though. – walnut Mar 02 '20 at 13:48
  • @walnut I was trying to avoid passing vectors or arrays... however, I may be able to get around it by using the containers' initialize_list constructors... – Francis Cugler Mar 02 '20 at 13:51
  • Yes, you can "have multiple amounts of inputs" for an object. This is done using a variadic parameter pack. One parameter pack. Two parameters packs cannot be deduced, like this. Sorry, C++ does not work like that. – Sam Varshavchik Mar 02 '20 at 13:51
  • @SamVarshavchik Maybe in a future release it will... – Francis Cugler Mar 02 '20 at 13:52
  • Can we do it in two steps? pass one variadic pack to construct an tuple. Then we need a way to split the tuple into two arrays. Which can then be passed to the constructor? –  Mar 02 '20 at 13:52
  • I appreciate the feedback... – Francis Cugler Mar 02 '20 at 14:14

3 Answers3

5

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.

walnut
  • 21,629
  • 4
  • 23
  • 59
  • That could work but, is there a way around having to explicitly state the types of the templates arguments when instantiating the object? Then again with there only ever being two distinct types, then it may not really be that much of an issue either... – Francis Cugler Mar 02 '20 at 13:53
  • 1
    @FrancisCugler Yes, I will add a solution for that. – walnut Mar 02 '20 at 13:54
  • 1
    @FrancisCugler Stating with C++17, `MyObject obj({a, b, c}, {x, y});` shoud work thanks to [CTAD](https://en.cppreference.com/w/cpp/language/class_template_argument_deduction) – NathanOliver Mar 02 '20 at 13:54
  • OP seems even not need template as `T1` is fixed as `In`, and `T2` is `Out`. – Jarod42 Mar 02 '20 at 14:00
  • @NathanOliver I think it won't work if I let the constructor takes `std::vector` as in my previous example. I have fixed it to use `std::initializer_list`, which should work that way. – walnut Mar 02 '20 at 14:01
  • Just by looking at your example, I wouldn't even really need to template the class. I could just have the class types within the container declarations and the constructor's parameters directly... – Francis Cugler Mar 02 '20 at 14:01
  • @Jarod42 Ah ok, I didn't realize that. I am going to leave this more general version up. OP can easily simplify it to his use case. – walnut Mar 02 '20 at 14:03
  • @FrancisCugler Yes, you can remove the template parameters if you always want the two specific types in your example. – walnut Mar 02 '20 at 14:03
2

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.

max66
  • 65,235
  • 10
  • 71
  • 111
-1

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.

Francis Cugler
  • 7,788
  • 2
  • 28
  • 59
  • 1
    You fixed the previous issues I mentioned, but sadly `std::initializer_list` doesn't work with move-only types. So no matter how you are going to try to implement this, it won't work for `std::unique_ptr` without templates. I suggest you make a new question asking about that specifically if you need it. I know an alternative implementation, but it would be off-topic here, I think. – walnut Mar 02 '20 at 15:57