1

In order to really understand C++17 fold expression, I have written a function append for containers :

#include <iostream>
#include <vector>
#include <list>

// fold expression
template<typename T, typename U, template<typename> typename... Args>
inline void append_impl(std::vector<T>& v, Args<U>... args) noexcept
{
  static_assert((std::is_constructible_v<T,U>));
  std::cout << "append_impl version one " << std::endl;
  (v.insert(std::end(v),
            std::begin(args),
            std::end  (args)), ...);
}

//fold expression
template<typename T, typename... Args>
inline void append_impl(std::vector<T>& v, Args&&... args) noexcept
{
  static_assert((std::is_constructible_v<T, Args&&> && ...));
  std::cout << "append_impl version two " << std::endl;
  (v.push_back(std::forward<Args>(args)), ...);
}
// fold expression
template<typename T, typename... Args>
inline void append(std::vector<T>& v, Args&&... args) noexcept
{
  (append_impl(v, args), ...);
}


int main()
{
std::vector<int> a = {1,2};
std::vector<int> b = {3,4};
std::vector<int> c = {5,6};
std::list<int> d = {15,16};

append(a,b,c, std::vector<int>{8,9}, 10, 11, 12, 13, 14, d);


for(const auto& e : a)
{
    std::cout << e << " ";
}
std::cout << std::endl;

return 0;
}

this works fine and give me the result :

append_impl version one

append_impl version one

append_impl version one

append_impl version two

append_impl version two

append_impl version two

append_impl version two

append_impl version two

append_impl version one

1 2 3 4 5 6 8 9 10 11 12 13 14 15 16

But I have a question from this code :

  • In the first version of append_impl, args is pass by copy. I wanted to use universal references (in Scott Meyers sens) to avoid this, but append_impl(std::vector<T>& v, Args<U>&&... args) give me a compilation error.
rog.cc: In instantiation of 'void append_impl(std::vector<T>&, Args&& ...) [with T = int; Args = {std::vector<int, std::allocator<int> >&}]':
prog.cc:18:15:   required from 'void append(std::vector<T>&, Args&& ...) [with T = int; Args = {std::vector<int, std::allocator<int> >&, std::vector<int, std::allocator<int> >&, std::vector<int, std::allocator<int> >, int, int, int, int, int, std::__cxx11::list<int, std::allocator<int> >&}]'
prog.cc:29:59:   required from here
prog.cc:10:3: error: static assertion failed
   10 |   static_assert((std::is_constructible_v<T, Args&&> && ...));
      |   ^~~~~~~~~~~~~
prog.cc:12:15: error: no matching function for call to 'std::vector<int>::push_back(std::vector<int>&)'
   12 |   (v.push_back(std::forward<Args>(args)), ...);

Why and what can I do to avoid copy ?

Community
  • 1
  • 1
Kafka
  • 720
  • 6
  • 21

1 Answers1

1

Following issues with your code:


You forgot to #include<type_traits>.


You forgot to forward in append:

(append_impl(v, std::forward<Args>(args)), ...);

Your template template parameter in append_impl is a bit problematic. Because it is written as template<typename> typename... Args it assumes that the template passed to it takes one template parameter. std::list (and other containers) take multiple template arguments (although the other ones are defaulted).It is not entirely clear whether such a template should be valid for your template template parameter.

GCC accepts it, while Clang doesn't (see https://godbolt.org/z/LY9r-k). I think that after resolution of CWG issue 150 GCC is correct in accepting the code, but I haven't checked in detail.

In any case, this problem can be easily avoided by changing the parameter to template<typename...> typename... Args, which accepts templates with arbitrarily many parameters.


Your actual issue: Universal references (or forwarding references) do only work if a template parameter T is directly used as T&& in a function parameter. If you use it inside a function parameters' template argument, the rules for reference collapse resulting in the forwarding behavior of universal references do not apply.

Args<U>&&... args is always a rvalue reference, no matter what reference qualification U has.

Therefore your code does not compile if you try to forward a lvalue reference to this function template which always expects a rvalue reference.

You should probably write one template overload for rvalue references and one for const lvalue references:

template<typename T, typename U, template<typename...> typename... Args>
inline void append_impl(std::vector<T>& v, const Args<U>&... args) noexcept

template<typename T, typename U, template<typename...> typename... Args>
inline void append_impl(std::vector<T>& v, Args<U>&&... args) noexcept

Then you have however the problem that a non-const lvalue reference will be a better match for the second template overload taking Args&& directly, so you need to add another overload:

template<typename T, typename U, template<typename...> typename... Args>
inline void append_impl(std::vector<T>& v, Args<U>&... args) noexcept 
{
    append_impl(v, std::as_const(args)...);
}

(requires #include<utility>)


In general the way you are trying to identify containers is not save. It will fail for all types that are formed from template classes and are not containers.

Instead you could select the correct function overload by SFINAE on the container interface that you are going to use, i.e. std::begin(arg), std::end(arg) and the insert call. You can use expression SFINAE to do so.

Alternatively you can write a type trait for the well-formedness of these expressions and use a single overload for append_impl that chooses the implementation based on a if constexpr checking the type trait.

In C++20 this will be doable in a much simpler way, using concepts.

There also doesn't seem to be any reason that append_impl takes Args as a parameter pack. It is only ever called with one parameter for Args.

walnut
  • 21,629
  • 4
  • 23
  • 59