Home > Blockchain >  How to forward C base & member initializers through a template constructor?
How to forward C base & member initializers through a template constructor?

Time:03-14

I'd like to make a template whose parameters B and D are the base class and elements of a fixed array in the template instantiation:

template <class B,class D>
struct fix_array : public B {
    D    myArray[5];
    /**/ fix_array( void ) { cout << "completely default constructed"; }
};

This works fine. Next, I'd like to pass initializing values to B:

template <class... BI>
/**/    fix_array( BI &&... bi )
           : B( std::forward<BI>(bi)... )
           , myArray( )
           {  cout << "This works too, even for multi-argument constructors of B."; }

That works too. And finally, my question:

How can I pass initializing values to D myArray[5]; through a template constructor?

I envision calling code looking something like:

fix_array<B,D>  myVal( { B-initializers }, { { D[0]-initializers }, { D[1]-initializers } } );

but I'm lost in variadic-template hell. Do all the D initializers have to have the same form? (Suppose D can be initialized with int, const char *, or D &&.)

Here is my test program. debug-util.hh defines a class Vobject that prints out information when it's constructed, destructed, copied, etc.

#include <stdio.h>
#include <memory.h>
#include <iostream>
#include <typeinfo>
#include <memory>
#include <vector>

#include "debug-util.hh"     // Defines Vobject and some other info-printing utilities

template <class B, class T>
class fix_array : public B
{
  public:
    T            pData[5];

    /**/         fix_array( void ) : B( ), pData( ) { std::cerr << "completely default construction" << std::endl; }

    template <class... BI>
    /**/         fix_array( BI &&... bi )
                   : B( std::forward<BI>(bi)... )
                   , pData( )
                   {
                       std::cerr << "fix_array Base was initialized" << std:: endl;
                   }
} ;

/*
 * A Vobject has 4 constructors: (void), (int), (int,char*), (int,int,int)
 *
 * It prints information about how it's constructed, when it's copied or moved,
 * and when it's destroyed.
 */

int
main( int, char ** )
{
    fix_array<Vobject,Vobject>   a;                 // These 4 initialize the Base Vobject
    fix_array<Vobject,Vobject>   b( 1 );            // using the various constructors.
    fix_array<Vobject,Vobject>   c( 3, "foobar" );
    fix_array<Vobject,Vobject>   d( 100, 200, 300 );

    // HOW CAN I PASS INITIALIZERS TO THE pDATA ARRAY MEMBERS???

    // fix_array<Vobject,Vobject> x( { 123, "base init" }, { { 1 }, { 2, "xyz" }, { 2, 4, 5 } } );

    std::cerr << "Hello, world" << std::endl;

    return 0;
}

(Yes, I understand that this is a silly program and it would be smarter to use std::vector and so on. But I have a more-complicated end goal, and learning how to use variadic templates would help me reach it.)

Execution of this program:

Vobject@A000()
Vobject@A001()
Vobject@A002()
Vobject@A003()
Vobject@A004()
Vobject@A005()
completely default construction
Vobject@A006(1)
Vobject@A007()
Vobject@A008()
Vobject@A009()
Vobject@A010()
Vobject@A011()
fix_array Base was initialized
Vobject@A012(3, "foobar")
Vobject@A013()
Vobject@A014()
Vobject@A015()
Vobject@A016()
Vobject@A017()
fix_array Base was initialized
Vobject@A018(100,200,300)
Vobject@A019()
Vobject@A020()
Vobject@A021()
Vobject@A022()
Vobject@A023()
fix_array Base was initialized
Hello, world
~Vobject@A023 (was constructed Vobject(void))
~Vobject@A022 (was constructed Vobject(void))
~Vobject@A021 (was constructed Vobject(void))
~Vobject@A020 (was constructed Vobject(void))
~Vobject@A019 (was constructed Vobject(void))
~Vobject@A018 (was constructed Vobject(int,int,int))
~Vobject@A017 (was constructed Vobject(void))
~Vobject@A016 (was constructed Vobject(void))
~Vobject@A015 (was constructed Vobject(void))
~Vobject@A014 (was constructed Vobject(void))
~Vobject@A013 (was constructed Vobject(void))
~Vobject@A012 (was constructed Vobject(int,const char*))
~Vobject@A011 (was constructed Vobject(void))
~Vobject@A010 (was constructed Vobject(void))
~Vobject@A009 (was constructed Vobject(void))
~Vobject@A008 (was constructed Vobject(void))
~Vobject@A007 (was constructed Vobject(void))
~Vobject@A006 (was constructed Vobject(int))
~Vobject@A005 (was constructed Vobject(void))
~Vobject@A004 (was constructed Vobject(void))
~Vobject@A003 (was constructed Vobject(void))
~Vobject@A002 (was constructed Vobject(void))
~Vobject@A001 (was constructed Vobject(void))
~Vobject@A000 (was constructed Vobject(void))

CodePudding user response:

I suspect you got stuck when trying to pass two packs at the same time. You likely got stuck there because it's the intuitive thing to do. Unfortunately, intuition fails us here because pack deduction from bare function arguments behaves in ways intuition doesn't expect. Trailing packs behave differently, there is difficulty in knowing when the second pack starts and the first ends, and the errors one gets along the way aren't always clear.

In situations like these I like to simply do what the standard library does. std::pair is a type that aggregates two elements. And it allows initializing each one individually (in place) by passing a tuple of parameters to each element (constructor 8). This synergizes with std::forward_as_tuple, a standard function template the does forwarding. You can supply arguments to both the base and the array be doing something similar.

The remaining question is only how to extract the tuple elements. B is easy in C 17's std::make_from_tuple. myArray doesn't have the same benefit, since c-style arrays are irregular types. We can however construct the array with a delegating constructor and an utility std::index_sequence. I'll be using this approach for B as well, thus requiring only C 14 in total.

template <class B, class T>
class fix_array : public B
{
public:
    template <class... BRefs, class... DRefs>
    fix_array(std::tuple<BRefs...> bArgs, std::tuple<DRefs...> dArgs)
    : fix_array(std::move(bArgs), std::make_index_sequence<sizeof...(BRefs)>{}, std::move(dArgs), std::make_index_sequence<sizeof...(DRefs)>{})
    {}

private:
    T            pData[5];

    template <class... BRefs, std::size_t... Bidx, class... DRefs, std::size_t... Didx>
    fix_array(std::tuple<BRefs...> bArgs, std::index_sequence<Bidx...>, std::tuple<DRefs...> dArgs,  std::index_sequence<Didx...>)
    : B( std::get<Bidx>(bArgs)... )
    , pData{ std::get<Didx>(dArgs)... }
    {
        std::cerr << "fix_array Base was initialized" << std:: endl;
    }
};

The two index_sequence parameters are tag type placeholders that contain the index of every tuple element in order. They are simply there to expand upon and access each tuple element at the correct ordinal. std::get already extract the elements by correct reference type after std::forward_as_tuple has done its thing.

You can see it working in a live example.

  • Related