Variadic template queries

Viewed 106

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?
1 Answers

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.

Related