PeWe
PeWe

Reputation: 99

Parameter Pack with alternating types

I have a struct C which gets initialized with a variable number of instances of struct A and struct B. E.g.:

struct A
{};

struct B
{};

struct C
{
    C(A&& o1, B&& p1, A&& o2)
    {}
    C(A&& o1, B&& p1, A&& o2, B&& p2, A&& o3)
    {}
    C(A&& o1, B&& p1, A&& o2, B&& p2, A&& o3, B&& p3, A&& o4)
    {}
    C(A&& o1, B&& p1, A&& o2, B&& p2, A&& o3, B&& p3, A&& o4, B&&p4, A&& o5)
    {}
};

So, rather than providing multiple ctor's with different number of parameters I would like to find something generic. However, the number of ctor parameters always grows about two parameters: B&& and A&&. Could this be accomplished using parameter packs. Or would be another solution without implementing for each number of parameters an according ctor?

The goal should be that struct C can be constructed like the following examples:

C c1 = { A(), B(), A() };
C c2 = { A(), B(), A(), B(), A(), B(), A() };

etc.

Upvotes: 9

Views: 672

Answers (3)

Francis Cugler
Francis Cugler

Reputation: 7925

Maybe something like this would help...

#include <iostream>
#include <vector>
#include <utility>

// Simple classes A & B to represent your alternating pattern classes.
class A{ public: int a; };
class B{ public: int b; };

// helper class template to act as a single parameter kind of like std::pair...
template<typename T, typename U>
class Pack{
private:
    T t_;
    U u_;

public:
    Pack( T&& t, U&& u ) : 
      t_( std::move( t ) ),
      u_( std::move( u ) )
    {}

    T getT() const { return t_; }
    U getU() const { return u_; }        
};

// your class with varying amount of parameters for its ctors
template<class T, class U>
class  C{
private:
    std::vector<Pack<T,U>> packs_;

public:
    template<typename... Packs>
    C( Packs&&... packs  ) : packs_{ std::move( packs )... } { }

    std::vector<Pack<T,U>> getPacks() const {
        return packs_;
    }
};

// A few overloaded ostream operator<<()s for easy printing...
std::ostream& operator<<( std::ostream& os, const A& a ) {
    os << a.a;
    return os;
}

std::ostream& operator<<( std::ostream& os, const B& b ) {
    os << b.b;
    return os;
}

template<typename T, typename U>
std::ostream& operator<<( std::ostream& os, const Pack<T,U>& pack ) {
    os << pack.getT() << " " << pack.getU() << '\n';
    return os;
}

// Main program to demonstrate its use
int main() {    
    Pack<int,double> p1( 1, 2.3 ), p2( 4, 9.2 ), p3( 5, 3.5 );        
    C<int, double> c( p1, p2, p3 );    
    for (auto& p : c.getPacks() )
        std::cout << p;

    std::cout << '\n'; 

    Pack<float, char> p4( 3.14f, 'a' ), p5( 6.95f, 'b' ),
                      p6( 2.81f, 'c' ), p7( 8.22f, 'd' );
    C<float, char> c2( p4, p5, p6, p7 );
    for ( auto& p : c2.getPacks() )
        std::cout << p;    

    return 0;
}

Working Code

-Output-

1 2.3
4 9.2
5 3.5

3.14 a
6.95 b
2.81 c
8.22 d

-Note- I did not incorporate for any odd number of parameters. For a more detailed solution with odd cases you can refer to the other answers with SFINAE or Delegating Constructor.

Upvotes: 0

palotasb
palotasb

Reputation: 4668

You can use a variadic template and SFINAE to enable only the constructor where the type parameters satisfy your (or any arbitrary) condition.

#include <type_traits>
struct A {};
struct B {};

You need type_traits for std::false_type and std::true_type.

The alternates template is the key. The goal is to make alternates<X, Y, T1, T2, T3, ..., Tn> inherit from std::true_type if and only if the T1, ... Tn list is alternating X and Y. The default choice (just below) is no, but we specialize for matching cases.

template <typename X, typename Y, typename... Ts>
struct alternates : std::false_type {};

I choose to make this template more generic than your requirement here and allow alternates<X, Y> to inherit from true_type. The empty list satisfies the mathematical requirement that all elements of it alternate. This will be a good stopgap for the recursive definition below.

template <typename X, typename Y>
struct alternates<X, Y> : std::true_type {};

Any other list of alternates<X, Y, Ts...> alternates if and only if Ts... minus the first element alternate Y and X (Y first!).

template <typename X, typename Y, typename... Ts>
struct alternates<X, Y, X, Ts...>
: alternates<Y, X, Ts...> {};

struct C
{

We define the constructor as a template that first takes a parameter pack (the type will be deduced, no need to specify when calling) and it has a defaulted template parameter for SFINAE purposes. If the defaulted argument cannot be calculated based on the parameter pack, the constructor will not exist. I added the extra conditions about the number of pairs which I assumed from the example.

    template<typename... Ts,
        typename = typename std::enable_if<
            sizeof...(Ts) % 2 == 1 &&
            sizeof...(Ts) >= 3 && // did you imply this?
            alternates<A, B, Ts...>::value
        >::type>
    C(Ts&&...);
};

The way SFINAE works is that std::enable_if only defines the std::enable_if<condition, T>::type (the ::type part) if condition is true. That can be any arbitrary boolean expression computable at compile time. If it is false, saying ::type at the end will be a substitution failure and the overload where you tried to use it (e.g., C{A(), A(), A()}) will simply not be defined.

You can test that the examples below work as expected. The ones commented out are not expected to work.

int main() {
    C c1 { A(), B(), A() };
    C c2 { A(), B(), A(), B(), A(), B(), A() };
    // C c3 {};                     // I assumed you need at least 2
    // C c4 { A(), B(), A(), A() }; // A, A doesn't alternate
    // C c5 { B(), A(), B() };      // B, A, B not allowed
    // C c6 { A(), B(), A(), B() }; // A, B, A, B doesn't pair
}

Try the code here.

Upvotes: 6

max66
max66

Reputation: 66230

I suppose you can use a template delegating constructor

Something as follows

#include <utility>

struct A {};
struct B {};

struct C
 {
   C (A &&)
    { }

   template <typename ... Ts>
   C (A &&, B &&, Ts && ... ts) : C(std::forward<Ts>(ts)...)
    { }
 };

int main()
 {
   C(A{});
   C(A{}, B{}, A{});
   C(A{}, B{}, A{}, B{}, A{});
   C(A{}, B{}, A{}, B{}, A{}, B{}, A{});
 }

If you require at least three element (so no C(A{}) but at least C(A{}, B{}, A{})) the not-template constructor become

   C (A &&, B &&, A&&)
    { }

Upvotes: 5

Related Questions