Workaround for not being able to move from initializer_list?
Asked Answered
R

1

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://mcmap.net/q/179830/-initializer_list-and-move-semantics, 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?

Rectangular answered 20/3, 2023 at 22:55 Comment(14)
"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.Inductance
N calls to push_back seems less than ideal, but the next best alternative seems to be to construct a std::array<T,N> and then move from that to the vector, which doesn't seem a whole lot better.Scend
The given solution could be simplified a little bit with fold expressions, but that's C++17.Jacobus
ranges::generate_n?Balls
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>(ts)),0) uses a comma operator to execute the push_back before returning a 0 from the expression.Kuhl
@Kuhl 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?Rectangular
@ThomasMcLeod: That’s a cast to void of an array prvalue, not a function declaration.Equipollent
@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 en.cppreference.com/w/cpp/language/direct_initializationKuhl
@ThomasMcLeod: It’s a function-style 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).Equipollent
@DavisHerring, thanks for the comment above. What version of the standard are you referencing?Kuhl
@ThomasMcLeod: A very recent draft (thus essentially C++23).Equipollent
@Kuhl void(expand { 0, (result.push_back(std::forward<Ts>(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 initializerFloorer
I'm actually more curious why we need that 0, before foldFloorer
@Swift-FridayPie, "I'm actually more curious why we need that 0, before fold" probably to ensure that the initializer-list is never empty.Kuhl
C
6

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.

Conjugal answered 29/3, 2023 at 19:47 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.