None-recursive variadic templates with std::initializer_list

Zoppo
CERTIFIED EXPERT
Published:
Updated:
Edited by: Andrew Leniart
This article is about the variadic template, which often is implemented in a way the compiler has to recurse through function calls and to create unneeded function.

There's a way to avoid compile-time recursion using std::initializer_list.

For some time now I've been spending a lot of time learning new features from C++14/C++17, and since in my opinion there are far too few articles about it here, I've decided to share a few newly learned things here.

This article is about variadic templates, which in my opinion is one of the greatest new features because it makes it very easy to write classes and functions that can be used very flexibly.


Note 1: I didn't use any #include in the code below, I guess everyone who understands this article knows, which headers are needed.


Note 2: In the code below I want to set the focus on variadic templates, so I do not use things like explicit move-semantic, forwarding or universal references here because it would make the code samples harder to read. IMO move-semantics, forwarding, and universal references are another subject, and I hope someone else is writing an article about it.


Disclaimer: The code may contain errors, and I'm sure there are things which could be done better. Please don't hesitate to tell me when you think you found a bug or a better way to do something.


First I show a very generic case of a variadic template to demonstrate how it is intended to be used.

Consider this example:

// function template to print out one single value
template < typename T >
void print( T&& val )
{
    std::cout << val;
}

// function template to print out the first passed argument
// and to 'call' itself recursively with remaining elements
template < typename T, typename ... ARGS >
void print( T&& val, ARGS&& ... args )
{
    print( val );
    print( std::forward< ARGS >( args ) ... );
}

int main()
{
    print( "Hello," , std::string{ " world: " }, 42, 3.1415, '\n' );
}

This is a pretty easy example to demonstrate how powerful variadic templates are.

But there's one drawback with this: This kind of implementing variadic templates generates compile-time recursion, each time the variadic print is called it picks out the first element and re-calls itself with the remaining arguments thus creating a new function.

In the given example this means the call to print in main will make the compiler creating these functions:


// these are of course needed, they're created from the none-variadic template for each used type
void print( char ) { ... }
void print( double ) { ... }
void print( int ) { ... }
void print( std::string ) { ... }
void print( const char* ) { ... }

// this is even needed, it's created from the function call
void print( const char*, std::string, int, double, char ) { ... }

// these are created due to compile-time recursion
void print( double, char ) { ... }
void print( int, double, char ) { ... }
void print( std::string, int, double, char ) { ... }


Now there's a kind of a trick to get this implemented without need of compile-time recursion. For this we use a std::initializer_list like this (description will follow):


template < typename ... ARGS >
void println( ARGS&& ... args )
{
    // the (void) is just used to avoid a 'expression result unused' warning
    (void)std::initializer_list< int >{ ( std::cout << args, 0 ) ... };
    std::cout << '\n';
}


The trick is to put the functionality which should be used for each single argument as statements to the constructor of a temporarily created instance of a std::initializer_list< int > - the compiler's pack expansion (this is the functionality used to replace the ... by arguments used in a function call) creates a comma-separated list of everything which is inside of the parenthesis.

So for each parameter passed to the function one int is added to the comman-seperated initialization list which is passed to the constructor of std::initializer_list in a form of { ( arg1, 0 ), ( arg2, 0 ), ( arg3, 0 ) /*,a.s.o*/ ).

Each of the inner statements evaluates to 0 (this is the value passed to the initialization list-constructor), but an arbitrary number of statements can be evaluated before the comma in front of the 0 - this is the place where the functionality (std::cout << argx) is executed.

Fortunately, the way this is implemented ensures everything is executed in the same order because a comma-separated list of statements is guaranteed to be evaluated from left to right (since comma-operators are sequence points).

So it's even possible to safely use multiple functions with each argument, thus allowing us to do quite complicated things like i.e. a function, which creates a vector of strings from an arbitrary number of arguments of arbitrary types:


template < typename T >
std::string make_string( const T& val )
{
    std::stringstream ss;
    ss << val;
    return ss.str();
}

template < typename ... ARGS >
std::vector< std::string > make_string_list( ARGS&& ... args )
{
    std::vector< std::string > v;
    std::string s;
    std::initializer_list< int >{
        (
            s = make_string( args ),
            std::clog << "Adding: '" << args << "' [" << typeid( args ).name() << "]",
            v.push_back( s ),
            std::clog << " done - verify: '" << v.back() << "'\n",
            0) ...
    };
    return v;
}

int main()
{
    auto l = make_string_list( "Hello", ',', std::string{ " world: " }, 42, 3.1415 );
}


I know, the syntax is horrible, but hey, this is really, really useful (and efficient too) - here's the logged output:


Adding: 'Hello' [char const [6]] done - verify: Hello
Adding: ',' [char] done - verify: ,
Adding: ' world: ' [class std::basic_string<char,struct std::char_traits<char>,class std::allocator<char> >] done - verify:  world:
Adding: '42' [int] done - verify: 42
Adding: '3.1415' [double] done - verify: 3.1415


IMO this is quite cool.

That's the theoretical part, as some kind of reward I want to share an implementation I made which I use myself often since it makes my life much easier.



Note: The following code will only be of special interest for those who work on GUI implementing with VisualStudio and MFC. If you don't use these you can jump to the end - sorry.



There was one thing I found annoying and boring whenever I had to do: filling report-style CListCtrls and updating items.

With variadic templates, I managed to make this much easier.

I'll first show samples about how I do things now, the code I need for it follows below.

Consider having a Dialog class with a CListCtrl-member mapped to a list control, in OnInitDialog this list should be filled with data elements like this:


struct data
{
    int id = 0;
    std::string name;
    double val = 1.0;
    bool state = true;
};

std::vector< data > m_data;


My common way to do this before was somehow like this:


class CTestDlg : public CDialog
{
    // ...
    CListCtrl m_list;
};

BOOL CTestDlg::OnInitDialog()
{
    // ...
    m_list.InsertColumn( 0, "id" );
    m_list.InsertColumn( 1, "name" );
    m_list.InsertColumn( 2, "val" );
    m_list.InsertColumn( 3, "state" );

    for ( auto data : m_data )
    {
        double v1 = GetTestVal();
        double v2 = GetTestVal();

        int index = m_list.InsertItem( m_list.GetItemCount(), std::to_string( m_data.id ).c_str() );
        m_list.SetItemText( n, 1, data.name.c_str() );
        m_list.SetItemText( n, 2, std::to_string( m_data.val ).c_str() );
        m_list.SetItemText( n, 3, std::to_string( m_data.state ).c_str() );
    }
}


With my extension I now can do it this way:


class CTestDlg : public CDialog
{
    // ...
    CExtListCtrl m_list; // my improved class
};

BOOL CTestDlg::OnInitDialog()
{
    m_list.CreateColumns( "id", "name", "val", "state" );

    for ( auto data : m_data )
    {
        m_list.AddRow( m_data.id, m_data.name, m_data.val, m_data.state );
    }
}


Cool, isn't it?

Further with my extension, I can set existing items, even partial, i.e.:


void CTestDlg::ResetValues()
{
    for ( int n = 0; n < m_list.GetItemCount(); n++ )
    {
        m_list.SetRowItems( n, nullptr, nullptr, 1.0, true );
    }
}


This replaces all items in the third and the fourth row while the first two rows are untouched.

And, as last I decided to use this all to implement a kind of type-safe CListCtrl which can be used like this:


class CTestDlg : public CDialog
{
    // ...
    CTypedListCtrl < int, std::string, double, bool > m_dynList;
    std::vector< data > m_data;
};

BOOL CTestDlg::OnInitDialog()
{
    m_list.CreateColumns( "id", "name", "val", "state" );
   
    m_list.AddRow( 1, "Test", -1.0, false ); // ok
    m_list.AddRow( "2", "Test", -1.0, false ); // compile error: cannot convert argument 1 from 'const char [1]' to 'int'
}




I really like this, since I have this CListCtrls stopped bothering me - here's the header-only code of my implementation:


// First here are some general helper functions to make using CListCtrl more convenient

namespace CListCtrlHelper
{
    inline void Reset( CListCtrl& ctrl )
    {
        ctrl.DeleteAllItems();

        if ( CHeaderCtrl* pHeader = ctrl.GetHeaderCtrl() )
        {
            while ( pHeader->GetItemCount() > 0 )
            {
                ctrl.DeleteColumn( 0 );
            }
        }
    }

    inline unsigned int GetColumnCount( CListCtrl& ctrl )
    {
        return nullptr == ctrl.GetHeaderCtrl() ? 0 : ctrl.GetHeaderCtrl()->GetItemCount();
    }

    // Info: the parameter 'bSet' is for convenience: it is useful in case this is called
    // from a function which itself has a flag whether to use SetRedraw-mechanism at all,
    // instead of the need to check such a flag in the callee the flag can simply passed.
    inline void SetListCtrlRedraw( CListCtrl& ctrl, const bool bRedraw = true, const bool bSet = true )
    {
        if ( false == bSet )
        {
            return;
        }

        ctrl.SetRedraw( false != bRedraw );

        if ( nullptr != ctrl.GetHeaderCtrl() )
        {
            ctrl.GetHeaderCtrl()->SetRedraw( false != bRedraw );
        }

        if ( false != bRedraw )
        {
            ctrl.Invalidate();
            ctrl.UpdateWindow();
        }
    }

    inline void    AutoAdjustColumnWidths( CListCtrl& ctrl, bool bSetRedraw = true )
    {
        CHeaderCtrl* pHeader = ctrl.GetHeaderCtrl();

        if ( nullptr != pHeader )
        {
            SetListCtrlRedraw( ctrl, bSetRedraw, false );

            for ( int i = 0; i < pHeader->GetItemCount(); i++ )
            {
                // find max of content vs header
                ctrl.SetColumnWidth( i, LVSCW_AUTOSIZE );
                int nColumnWidth = ctrl.GetColumnWidth( i );

                ctrl.SetColumnWidth( i, LVSCW_AUTOSIZE_USEHEADER );
                int nHeaderWidth = ctrl.GetColumnWidth( i );

                // set width to max
                ctrl.SetColumnWidth( i, max( nColumnWidth, nHeaderWidth ) );
            }

            pHeader->RedrawWindow( nullptr, nullptr, RDW_ALLCHILDREN | RDW_ERASE | RDW_FRAME | RDW_UPDATENOW );

            SetListCtrlRedraw( ctrl, bSetRedraw, true );
        }
    }
} // CListCtrlHelper


Here the basic variadic templates are implemented:


namespace CExtListCtrlHelper
{
    using namespace CListCtrlHelper;

    template < typename T >
    std::string ToString( T param, const std::locale loc = std::locale::classic() )
    {
        std::stringstream ss;
        ss.imbue( loc );
        ss << param;
        return std::move( ss.str() );
    }

    inline void AppendColumn( const unsigned int col, CListCtrl& ctrl, const char* param )
    {
        ctrl.InsertColumn( col, param );
    }

    inline void AppendColumn( const unsigned int col, CListCtrl& ctrl, const std::string& param )
    {
        AppendColumn( col, ctrl, param.c_str() );
    }

    template < typename T >
    inline void AppendColumn( const unsigned int col, CListCtrl& ctrl, T param )
    {
        AppendColumn( col, ctrl, std::move( ToString( param ) ) );
    }

    template < typename ... ARGS >
    inline void AppendColumns( const unsigned int col, CListCtrl& ctrl, ARGS ... args )
    {
        int count = -1;
        (void)std::initializer_list< int >{ ( AppendColumn( ( count = count + 1, col + count ), ctrl, args ), 0 ) ... };
    }

    template < typename ... ARGS >
    inline void    CreateColumns( CListCtrl& ctrl, ARGS ... args )
    {
        Reset( ctrl );
        AppendColumns( 0, ctrl, args ... );
    }

    inline void SetRowItem( const unsigned int row, const unsigned int col, CListCtrl& ctrl, std::nullptr_t )
    {
        /*do nothing: nullptr is used as placeholder*/
    }

    inline void SetRowItem( const unsigned int row, const unsigned int col, CListCtrl& ctrl, char* param )
    {
        ctrl.SetItemText( row, col, param );
    }

    inline void SetRowItem( const unsigned int row, const unsigned int col, CListCtrl& ctrl, std::string param )
    {
        ctrl.SetItemText( row, col, param.c_str() );
    }

    template < typename T >
    inline void SetRowItem( const unsigned int row, const unsigned int col, CListCtrl& ctrl, T param )
    {
        if ( col >= GetColumnCount( ctrl ) )
        {
            return;
        }

        SetRowItem( row, col, ctrl, std::move( ToString( param ) ) );
    }

    template < typename ... ARGS >
    inline void SetRowItems( const unsigned int row, const unsigned int col, CListCtrl& ctrl, ARGS ... args )
    {
        int count = -1;
        (void)std::initializer_list< int >{ ( SetRowItem( row, ( count = count + 1, col + count ), ctrl, args ), 0 ) ... };
    }

    template < typename ... ARGS >
    inline void SetRowItems( const unsigned int row, CListCtrl& ctrl, ARGS ... args )
    {
        SetRowItems( row, 0u, ctrl, args ... );
    }

    template < typename ... ARGS >
    inline const unsigned int InsertRow( const unsigned int row, CListCtrl& ctrl, ARGS ... args )
    {
        unsigned int index = ctrl.InsertItem( row, "", 0 );
        SetRowItems( index, ctrl, args ... );
        return index;
    }

    template < typename ... ARGS >
    inline const unsigned int AddRow( CListCtrl& ctrl, ARGS ... args )
    {
        return InsertRow( ctrl.GetItemCount(), ctrl, args ... );
    }
};


Next a CListCtrl-derived class which wraps these helpers:


class CExtListCtrl : public CListCtrl
{
public:
    void    Reset()
    {
        CExtListCtrlHelper::Reset( *this );
    }

    void    AutoAdjustColumnWidths( bool bSetRedraw = true )
    {
        CExtListCtrlHelper::AutoAdjustColumnWidths( *this, bSetRedraw );
    }

    template < typename T >
    void SetRowItem( const unsigned int row, const unsigned int col, T param )
    {
        CExtListCtrlHelper::SetItemText( *this, row, col, std::move( std::to_string( param ).c_str() ) );
    }

    template < typename T, typename ... ARGS >
    void SetRowItems( const unsigned int row, const unsigned int col, T param, ARGS ... args )
    {
        CExtListCtrlHelper::SetRowItem( *this, col, param, args ... );
    }

    template < typename T >
    void AppendColumn( const unsigned int col, T param )
    {
        CExtListCtrlHelper::AppendColumn( *this, col, std::move( std::to_string( param ) ) );
    }

    template < typename ... ARGS >
    void AppendColumns( const unsigned int col, std::string title, ARGS ... args )
    {
        CExtListCtrlHelper::AppendColumns( col, *this, title, args ... );
    }

    template < typename ... ARGS >
    void    CreateColumns( ARGS ... args )
    {
        CExtListCtrlHelper::CreateColumns( *this, args ... );
    }
};


And finally here's the CListCtrl-derived class CTypedListCtrl:


template < typename ... ARGS >
class CTypedListCtrl : public CExtListCtrl
{
public:
    unsigned int AddRow( ARGS ... args )
    {
        return CExtListCtrlHelper::AddRow( *this, args ... );
    }
};





That's all,


thanks for reading, I hope it was at least a bit informative for you,


best regards,



ZOPPO


PS: I learned most of this reading and watching tutorials, and I want to especially mention (and thank) Jason Turner who released a lot of very interesting video tutorials on YouTube, i.e. I found the trick with std::initializer_list in a video of his great C++ Weekly episodes at https://www.youtube.com/watch?v=VXi0AOQ0PF0&index=4&list=PLs3KjaCtOwSZ2tbuV1hx8Xz-rFZTan2J1

0
4,076 Views
Zoppo
CERTIFIED EXPERT

Comments (0)

Have a question about something in this article? You can receive help directly from the article author. Sign up for a free trial to get started.