Search code examples
c++c++17c++14variadic-templates

Variadic template queries


I am trying to understand below code. Copied directly from Jason Turner youtube video

#include <iostream>
#include <sstream>
#include <vector>

template<typename ...T>
std::vector<std::string> print(const T& ...t)
{
    std::vector<std::string> retval;
    std::stringstream ss;
    (void)std::initializer_list<int>{
      (
         ss.str(""),
         ss << t,
         retval.push_back(ss.str()),
         0)...
    };
    return retval;
}

int main()
{
    for( const auto &s : print("Hello", "World", 5.4, 1.1, 2.2) ) {
        std::cout << s << "\n";
    }
}

Questions :

  1. Can someone give the expanded view of the code within initializer_list? I am having hard time visualising how the statement expands per argument? Does the ss.str(""), ss << t and then the push_back happen for each parameter in the pack OR they are just executed once? I am not able to visualize how the expanded initializer list will look like?
  2. Why do we need the dummy '0' at the end of the initializer_list? What happens if i don't have that?
  3. How can i easily view the ... expansion in code i shared?

Solution

  • T and t are parameter packs.

    There are two primary ways of using a pack: a fold expression (in C++17 and newer) and just a regular pack expansion.

    A fold expression would look like this:

    ((ss.str(""), ss << t, retval.push_back(ss.str())), ...);
    

    Fold expression repeats its operand for each pack element, inserting some operator (, in this case) between parts belonging to each argument. The one above expands to:

    ((ss.str(""), ss << t1, retval.push_back(ss.str())), // <-- Inserted commas
     (ss.str(""), ss << t2, retval.push_back(ss.str())), // <--
     (ss.str(""), ss << t3, retval.push_back(ss.str())));
    

    A regular expansion is similar, except that it always generates a comma, and that comma can not be an operator (as opposed to e.g. a separator between array initializers, or function arguments).

    E.g. if you wrote (ss.str(""), ss << t, retval.push_back(ss.str()))...; under the assumption that it would work like that fold expression, it wouldn't work, because the resulting comma would have to be an operator.

    Because of this limitation, before C++17 people were using dummy arrays (or initializer_lists like in your example). Here's how it would look with an array:

    int dummy[] = {(ss.str(""), ss << t, retval.push_back(ss.str()), 0)...};
    

    This expands to:

    int dummy[] = {(ss.str(""), ss << t1, retval.push_back(ss.str()), 0),
                   (ss.str(""), ss << t2, retval.push_back(ss.str()), 0),
                   (ss.str(""), ss << t3, retval.push_back(ss.str()), 0)};
    

    Here, the size of the array (or initializer_list) matches the size of the pack.

    ,0 is necessary because each array element is an int, so it must be initialized with an int.