1

Recently I was learning about C++ and variadic templates. I tried to write a template function that takes a container (I simplified it and I only work with list in this question) and some other arguments and emplaces the other arguments to the container. My code looked like this:

#include <iostream>
#include <list>
#include <utility>

template <typename Container, typename... Args>
void my_emplace(Container &c, Args &&... args){
    c.emplace_back(std::forward<Args>(args)...);
}

int main(void) {
    std::list<int> l = {1, 2};
    my_emplace(l, 3, 4, 5);

    for (auto i : l)
        std::cout << i << ", ";
    return 0;
}

However, the code did not work:

$ g++ -std=c++17 test.cpp
...
/usr/include/c++/7/ext/new_allocator.h:136:4: error: new initializer expression list treated as compound expression [-fpermissive]
  { ::new((void *)__p) _Up(std::forward<_Args>(__args)...); }
    ^~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~

I found a solution on the internet -- the line c.emplace_back(std::forward<Args>(args)...); is replaced by (c.emplace_back(std::forward<Args>(args)), ...);. The code works perfectly fine with it.

This is the part that I do not understand. I have never seen this kind of syntax.

  • Why didn't my code work? This answer here basically says that it is correct.
  • What does the correct solution that I found actually do? I've never used a comma operator, but I found that when used, both expressions are evaluated and the result of the first one is discarded. What does it do here and why does the code not work without it?

Thank you for your answers.

Topper Harley
  • 375
  • 4
  • 17
  • you are calling emplace_back with 3 arguments, but 'int constructor' only takes 1 – Yamahari Jan 04 '20 at 12:06
  • Good question. Just a side note: use `vector` as your default container (unless you have a good reason to go for `list`), and don't use the letter `l` as an identifier, since it's really hard to see the difference with `1`. – xtofl Jan 04 '20 at 12:25
  • *in general don't use single letter identifiers unless they are well established like "x, y, z" for axes. – Yamahari Jan 04 '20 at 12:31
  • @xtofl @Yamahari : The code itself does not look like this, I shortened it to simplify my question. In the function body, I also tried to work with multiple containers like vector (``emplace_back``), set (``emplace``) etc. and make ``my_emplace`` "universal". – Topper Harley Jan 04 '20 at 13:00
  • Great! It's best to simplify in the direction of these good practices, too :). It leads to less distraction for picky readers, and will give you better answers. – xtofl Jan 04 '20 at 13:18

3 Answers3

2

Assume args is (1, 2, 3, 4), this call:

c.emplace_back(std::forward<Args>(args)...);

basically means:

c.emplace_back(1, 2, 3, 4); so you can't construct int with these arguments to emplace_back hence a compile error.

And this call:

(c.emplace_back(std::forward<Args>(args)), ...);

means

c.emplace_back(1), c.emplace_back(2), c.emplace_back(3), c.emplace_back(4);

Second call expands the whole expression with comma operator, its called Fold Expressions its added in C++17 you can read more about it Fold Expression

Gaurav Dhiman
  • 1,163
  • 6
  • 8
1

emplace_back will forward it's arguments to the constructor of the value_type, in this case int. That means you can call c.emplace_back(3), but not c.emplace_back(3,4,5).

When several values are passed, you want to call emplace_back several times.

c.emplace_back(3), c.emplace_back(4), c.emplace_back(5);

We can do this on one line, using the comma operator as above.

You two different ways of unfolding the parameter pack works in the same way.

c.emplace_back(std::forward<Args>(args)...);
// normal un-packing, translates to
c.emplace_back(3, 4, 5);

(c.emplace_back(std::forward<Args>(args)), ...);
// un-packing with a fold-expression using the comma operator
(c.emplace_back(3), c.emplace_back(4), c.emplace_back(5));
super
  • 12,335
  • 2
  • 19
  • 29
0

Starting from C++17, you can use fold expression (see super's and Gaurav Dhiman's answers).

Before C++17 (C++11 and C++14) you can obtains something similar expanding the expression initializing an unused array

I mean

template <typename Container, typename... Args>
void my_emplace (Container &c, Args && ... args)
 {
   using unused = int[];

   (void)unused { 0, ((void)c.emplace_back(std::forward<Args>(args)), 0)... };
 }

Observe the use of the comma operator to discard the expanded expression.

Observe also the (void) in front of c.emplace_back(). In this case (c is a std::list) is superfluous, but is added to avoid potential problems with returned objects redefining the comma operator.

max66
  • 65,235
  • 10
  • 71
  • 111