3

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?
Test
  • 564
  • 3
  • 12
  • 1
    (1) - `( )...` means that everything in `( )` is repeated for each argument. (2) - `0` is not "at the end", it's repeated for each argument. Since it's a `initializer_list`, each element must be an integer, `0` in this case. – HolyBlackCat Jun 14 '21 at 16:20
  • 3
    This doesn't look like C++17 code. C++17 would use fold expressions. – HolyBlackCat Jun 14 '21 at 16:21
  • If you want to see how templates can expand, you may want to use [C++ Insights](https://cppinsights.io/). – mediocrevegetable1 Jun 14 '21 at 16:23
  • @HolyBlackCat : What do you mean by this statement in your initial response " Since it's a initializer_list, each element must be an integer, 0 in this case.". What do we achieve by adding a zero? What will happen if i don't add that zero? Can you please give a simpler and independent example? – Test Jun 15 '21 at 03:19
  • @Test it just there so the whole expression result is type `int` (`push_back` return `void`) – apple apple Jun 15 '21 at 04:37
  • [how-does-the-comma-operator-work](https://stackoverflow.com/questions/54142/how-does-the-comma-operator-work) – apple apple Jun 15 '21 at 04:40
  • [Built-in_comma_operator](https://en.cppreference.com/w/cpp/language/operator_other#Built-in_comma_operator) – apple apple Jun 15 '21 at 04:40
  • @appleapple : Can you give full context in view of the problem please? I understood the comma operator precedence from the link. (1) Why did we even bother to use the initializer list in above code? All we wanted is to update the vector. isn't it? (2) And after using initializer list why we added a void ? Isnt the return value ignored itself if we don't capture it? – Test Jun 15 '21 at 04:54

1 Answers1

3

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.

HolyBlackCat
  • 78,603
  • 9
  • 131
  • 207
  • @HolyBlackCat - is it necessary to even declare dummy int array? Can't I just use an initializer list and remove the need for 0 ? Isn't there something called as blank initializer list like {} ? – Test Jun 15 '21 at 12:56
  • @Test I don't fully understand your idea, but no, you need either an array or an `initializer_list` (both with elements of a dummy type, e.g. `int`). – HolyBlackCat Jun 15 '21 at 13:06
  • @HolyBlackCat - My only doubt here is why do I need the dummy array OR dummy initializer list? What c++ rule is governing this requirement? – Test Jun 15 '21 at 13:08
  • @Test I tried to explain it in the answer, starting from "A regular expansion is similar ...". – HolyBlackCat Jun 15 '21 at 13:14