9

The convenient initializer_list syntax seems to come at a price of not being able to move members of the list, creating unnecessary copies.

struct  A
{
    // some members which are dynamic resources...
    A() { cout << "Default Constructor\n"; }
    A(const A& original) { cout << "Copy constructor\n"; }
    A(A&& original) { cout << "Move constructor\n";  }
};
    
int main() {
    vector<A> v1{ A() , A() }; // calls copy
    vector<A> v2;
    v2.push_back(A()); v2.push_back(A()); // calls move
    return 0;
}

If I understand correctly, this is because de-referencing the initializer iterators gives const T, which will be copied even when move is attempted.

Is there a workaround for this?

Reading https://stackoverflow.com/a/44307741/889742, a solution is proposed which uses variable argument templates, as follows:

template<class Array> struct maker;

// a maker which makes a std::vector
template<class T, class A>
struct maker<std::vector<T, A>>
{
  using result_type = std::vector<T, A>;

  template<class...Ts>
  auto operator()(Ts&&...ts) const -> result_type
  {
    result_type result;
    result.reserve(sizeof...(Ts));
    using expand = int[];
    void(expand {
      0,
      (result.push_back(std::forward<Ts>(ts)),0)...
    });

    return result;
  }
};

// a maker which makes std::array
template<class T, std::size_t N>
struct maker<std::array<T, N>>
{
  using result_type = std::array<T, N>;

  template<class...Ts>
  auto operator()(Ts&&...ts) const
  {
    return result_type { std::forward<Ts>(ts)... };
  }

};

//
// delegation function which selects the correct maker
//
template<class Array, class...Ts>
auto make(Ts&&...ts)
{
  auto m = maker<Array>();
  return m(std::forward<Ts>(ts)...);
}

( aside1: this declares an array , will it be optimized away?
aside2: what is the meaning and purpose of void in the expression? it seems unnecessary )

With new changes in C++20 and C++23, is there now a better workaround for all this?

Evg
  • 25,259
  • 5
  • 41
  • 83
Gonen I
  • 5,576
  • 1
  • 29
  • 60
  • 5
    "*If I understand correctly, this is because de-referencing the initializer iterators gives `const T`*" - that is because the underlying array that the `initializer_list` holds is a `const T[]` array. You can't move `const` objects, only copy them. – Remy Lebeau Mar 20 '23 at 23:17
  • N calls to `push_back` seems less than ideal, but the next best alternative seems to be to construct a `std::array` and then move from that to the `vector`, which doesn't seem a whole lot better. – Mooing Duck Mar 21 '23 at 00:12
  • The given solution could be simplified a little bit with fold expressions, but that's C++17. – Evg Mar 21 '23 at 00:20
  • `ranges::generate_n`? – 康桓瑋 Mar 21 '23 at 01:02
  • To answer one of your questions, the `void(expand{...});` statement that is part of the `operator()` method appears to be an unnamed function declaration returning `void` and taking an `int[]` parameter. The expression `(result.push_back(std::forward(ts)),0)` uses a comma operator to execute the push_back before returning a `0` from the expression. – ThomasMcLeod Mar 21 '23 at 01:13
  • @ThomasMcLeod Thanks. I had already figured all that out, except for the void part. What advantage is there in evaluating the expression as a parameter to a function declaration ( didn't even know you could do that ) rather than just the expression on its own? – Gonen I Mar 21 '23 at 02:30
  • @ThomasMcLeod: That’s a cast to `void` of an array prvalue, not a function declaration. – Davis Herring Mar 21 '23 at 02:52
  • @DavisHerring It's not the valid syntax for a cast. You might say that direct initialization is functionally a type of cast, but "In case of ambiguity between a variable declaration using the direct-initialization syntax (1) (with round parentheses) and a function declaration, the compiler always chooses function declaration. This disambiguation rule is sometimes counter-intuitive and has been called the most vexing parse." See notes under https://en.cppreference.com/w/cpp/language/direct_initialization – ThomasMcLeod Mar 21 '23 at 13:37
  • @ThomasMcLeod: It’s a [function-style cast](https://en.cppreference.com/w/cpp/language/explicit_cast) ([expr.type.conv]), which does often perform direct initialization. There’s no ambiguity here because a function declaration cannot omit the declarator-id ([dcl.decl.general]/5)—and because a *braced-init-list* can syntactically appear in a function parameter only after `=` ([dcl.fct]/3). – Davis Herring Mar 22 '23 at 02:54
  • @DavisHerring, thanks for the comment above. What version of the standard are you referencing? – ThomasMcLeod Mar 28 '23 at 00:49
  • @ThomasMcLeod: A very recent draft (thus essentially C++23). – Davis Herring Mar 28 '23 at 03:20
  • @ThomasMcLeod `void(expand { 0, (result.push_back(std::forward(ts)),0)... });` is big brother of a call statement, An empty statement would be equivalent to `void(0);`, a non-assigning function call statement would be `void(function(arguments)); ` Here we have `void(type{initializer-list});` which is meant to create statically unfolded loop based on that fold-expression inside of initializer – Swift - Friday Pie Mar 31 '23 at 14:29
  • I'm actually more curious why we need that `0,` before fold – Swift - Friday Pie Mar 31 '23 at 14:36
  • @Swift-FridayPie, "I'm actually more curious why we need that 0, before fold" probably to ensure that the initializer-list is never empty. – ThomasMcLeod Mar 31 '23 at 18:18

1 Answers1

5

If you can wait for C++23, ranges can help:

#include <array>
#include <ranges>
auto v= std::array{A(),A()}         //c++17
      | std::views::as_rvalue       //c++23
      | std::ranges::to<std::vector>(); //c++23

array constructor uses CTAD to deduce type from first input and size from number of inputs. Rest is self explanatory. Instead of constructor to_array can be used if only type must be explicitly specified:

#include <array>
#include <ranges>
auto v= std::to_array<A>({A(),A()}) //c++20
      | std::views::as_rvalue       //c++23
      | std::ranges::to<std::vector>(); //c++23

The compile time type <A> argument is not compulsory and can be deduced, iff all the arguments have the same type. If the exra culrly braces are annoying, std::make_array is under test. It is not shipped under the standard yet, but it will omit the curly braces that std::to_array needs. Another option would be to accept a raw array as function input and use std::move_iterator:

template<typename T, std::size_t N>
std::vector<T> to_vector(T (&&arr)[N]){
     return std::vector<T>(
            std::make_move_iterator(std::begin(arr)),//c++11
            std::make_move_iterator(std::end(arr)));
};

auto v = to_vector({A(),A()});

This one looks like the std::to_array version, but it is not part of std library - because arrays are special case. There are plenty of possible combinations, but almost all rely on copy elision - which started as an optimization option with C++11 and continued as mandatory since C++17.

Red.Wave
  • 2,790
  • 11
  • 17