103

Am I allowed to move elements out of a std::initializer_list<T>?

#include <initializer_list>
#include <utility>

template<typename T>
void foo(std::initializer_list<T> list)
{
    for (auto it = list.begin(); it != list.end(); ++it)
    {
        bar(std::move(*it));   // kosher?
    }
}

Since std::intializer_list<T> requires special compiler attention and does not have value semantics like normal containers of the C++ standard library, I'd rather be safe than sorry and ask.

fredoverflow
  • 237,063
  • 85
  • 359
  • 638
  • The core language defines that the object referred to by an `initializer_list` are *non*-const. Like, `initializer_list` refers to `int` objects. But I think that is a defect - it is intended that compilers can statically allocate a list in read only memory. – Johannes Schaub - litb Nov 19 '11 at 14:42

8 Answers8

96

No, that won't work as intended; you will still get copies. I'm pretty surprised by this, as I'd thought that initializer_list existed to keep an array of temporaries until they were move'd.

begin and end for initializer_list return const T *, so the result of move in your code is T const && — an immutable rvalue reference. Such an expression can't meaningfully be moved from. It will bind to an function parameter of type T const & because rvalues do bind to const lvalue references, and you will still see copy semantics.

Probably the reason for this is so the compiler can elect to make the initializer_list a statically-initialized constant, but it seems it would be cleaner to make its type initializer_list or const initializer_list at the compiler's discretion, so the user doesn't know whether to expect a const or mutable result from begin and end. But that's just my gut feeling, probably there's a good reason I'm wrong.

Update: I've written an ISO proposal for initializer_list support of move-only types. It's only a first draft, and it's not implemented anywhere yet, but you can see it for more analysis of the problem.

Potatoswatter
  • 126,977
  • 21
  • 238
  • 404
  • 11
    In case it isn't clear, it still means using `std::move` is safe, if not productive. (Barring `T const&&` move constructors.) – Luc Danton Nov 19 '11 at 09:46
  • I don't think that you could make the whole argument either `const std::initializer_list` or just `std::initializer_list` in a way that does not cause surprises quite often. Consider that each argument in the `initializer_list` can be either `const` or not and that is known in the context of the caller, but the compiler must generate just one version of the code in the context of the callee (i.e. inside `foo` it does not know anything about the arguments that the caller is passing in) – David Rodríguez - dribeas Nov 19 '11 at 10:34
  • 1
    @David: Good point, but it would still be useful to have a `std::initializer_list &&` overload do something, even if a non-reference overload is also required. I suppose it would be even more confusing than the current situation, which is already bad. – Potatoswatter Nov 19 '11 at 10:42
  • You may be interested by this article: http://cpptruths.blogspot.fr/2013/10/moving-elements-from-stl-containers-and.html – Jean-Bernard Jansen Mar 10 '14 at 01:10
  • 1
    @JBJansen It can't be hacked around. I don't see exactly what that code is supposed to accomplish wrt initializer_list, but as the user you do not have the needed permissions to move from it. Safe code will not do so. – Potatoswatter Mar 10 '14 at 01:37
  • Yes you can hack it with mutable values used correctly. Such haking may be dangerous as forward_as_tuple is if not used correctly. May be you will be interested in this: http://stackoverflow.com/questions/13957166/is-it-safe-to-move-elements-of-a-initializer-list – Jean-Bernard Jansen Mar 23 '14 at 19:09
  • 2
    @Potatoswatter, late comment, but what's the status of the proposal. Is there any remote chance it may make it into C++20? – WhiZTiM Jul 19 '17 at 11:23
  • @WhiZTiM I'm not so active in C++ lately (ISO or otherwise), but anything is possible. It's an open standard — if you want, you can promote the idea yourself! C++20 should certainly still be open to new ideas, since C++17 is yet to be ratified. – Potatoswatter Jul 25 '17 at 19:49
  • Has this proposal made any progress? I'm also fairly surprised that initializer lists force copies. – Michaël Feb 25 '21 at 19:16
21
bar(std::move(*it));   // kosher?

Not in the way that you intend. You cannot move a const object. And std::initializer_list only provides const access to its elements. So the type of it is const T *.

Your attempt to call std::move(*it) will only result in an l-value. IE: a copy.

std::initializer_list references static memory. That's what the class is for. You cannot move from static memory, because movement implies changing it. You can only copy from it.

Nicol Bolas
  • 378,677
  • 53
  • 635
  • 829
  • A const xvalue is still an xvalue, and `initializer_list` references the stack if that is necessary. (If the contents are not constant, it is still thread-safe.) – Potatoswatter Nov 20 '11 at 00:58
  • 5
    @Potatoswatter: You cannot move from a constant object. The `initializer_list` object itself may be an xvalue, but it's contents (the actual array of values that it points to) are `const`, because those contents may be static values. You simply cannot move from the contents of an `initializer_list`. – Nicol Bolas Nov 20 '11 at 01:09
  • See my answer and its discussion. He has moved the dereferenced iterator, producing a `const` xvalue. `move` might be meaningless, but it's legal and even possible to declare a parameter that accepts just that. If moving a particular type happens to be a no-op, it might even work correctly. – Potatoswatter Nov 20 '11 at 01:15
  • 1
    @Potatoswatter: The C++11 standard expends a lot of language ensuring that non-temporary objects are not actually moved unless you use `std::move`. This ensures that you can tell from inspection when a move operation happens, since it affects both the source and the destination (you don't want it to happen implicitly for named objects). Because of that, if you use `std::move` in a place where a move operation *doesn't* happen (and no actual movement will happen if you have a `const` xvalue), then the code is misleading. I think it's a mistake for `std::move` to be callable on a `const` object. – Nicol Bolas Nov 20 '11 at 01:19
  • 1
    Maybe, but I'll still take fewer exceptions to the rules over the possibility of misleading code. Anyway, that is exactly why I answered "no" even though it's legal, and the result is an xvalue even if it will only bind as a const lvalue. To be honest, I've already had a brief flirtation with `const &&` in a garbage-collected class with managed pointers, where everything relevant was mutable and moving moved the pointer management but didn't affect the contained value. There are always tricky edge cases :v) . – Potatoswatter Nov 20 '11 at 01:32
3

This won't work as stated, because list.begin() has type const T *, and there is no way you can move from a constant object. The language designers probably made that so in order to allow initializer lists to contain for instance string constants, from which it would be inappropriate to move.

However, if you are in a situation where you know that the initializer list contains rvalue expressions (or you want to force the user to write those) then there is a trick that will make it work (I was inspired by the answer by Sumant for this, but the solution is way simpler than that one). You need the elements stored in the initialiser list to be not T values, but values that encapsulate T&&. Then even if those values themselves are const qualified, they can still retrieve a modifiable rvalue.

template<typename T>
  class rref_capture
{
  T* ptr;
public:
  rref_capture(T&& x) : ptr(&x) {}
  operator T&& () const { return std::move(*ptr); } // restitute rvalue ref
};

Now instead of declaring an initializer_list<T> argument, you declare aninitializer_list<rref_capture<T> > argument. Here is a concrete example, involving a vector of std::unique_ptr<int> smart pointers, for which only move semantics is defined (so these objects themselves can never be stored in an initializer list); yet the initializer list below compiles without problem.

#include <memory>
#include <initializer_list>
class uptr_vec
{
  typedef std::unique_ptr<int> uptr; // move only type
  std::vector<uptr> data;
public:
  uptr_vec(uptr_vec&& v) : data(std::move(v.data)) {}
  uptr_vec(std::initializer_list<rref_capture<uptr> > l)
    : data(l.begin(),l.end())
  {}
  uptr_vec& operator=(const uptr_vec&) = delete;
  int operator[] (size_t index) const { return *data[index]; }
};

int main()
{
  std::unique_ptr<int> a(new int(3)), b(new int(1)),c(new int(4));
  uptr_vec v { std::move(a), std::move(b), std::move(c) };
  std::cout << v[0] << "," << v[1] << "," << v[2] << std::endl;
}

One question does need an answer: if the elements of the initializer list should be true prvalues (in the example they are xvalues), does the language ensure that the lifetime of the corresponding temporaries extends to the point where they are used? Frankly, I don't think the relevant section 8.5 of the standard addresses this issue at all. However, reading 1.9:10, it would seem that the relevant full-expression in all cases encompasses the use of the initializer list, so I think there is no danger of dangling rvalue references.

Marc van Leeuwen
  • 3,327
  • 20
  • 34
  • String constants? Like `"Hello world"`? If you move from them, you just copy a pointer (or bind a reference). – dyp Jul 07 '14 at 11:38
  • 1
    *"One question does need an answer"* The initializers inside `{..}` are bound to references in the function parameter of `rref_capture`. This does not extend their lifetime, they're still destroyed at the end of the full-expression in which they've been created. – dyp Jul 07 '14 at 11:42
  • Per [T.C.](http://stackoverflow.com/users/2756719/t-c)'s comment from another answer: If you have multiple overloads of the constructor, *wrap the `std::initializer_list>` in some transformation trait of your choosing - say, `std::decay_t` - to block unwanted deduction.* – Kuba hasn't forgotten Monica Oct 27 '16 at 13:03
2

I thought it might be instructive to offer a reasonable starting point for a workaround.

Comments inline.

#include <memory>
#include <vector>
#include <array>
#include <type_traits>
#include <algorithm>
#include <iterator>

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)...);
}

// vectors and arrays of non-copyable types
using vt = std::vector<std::unique_ptr<int>>;
using at = std::array<std::unique_ptr<int>,2>;


int main(){
    // build an array, using make<> for consistency
    auto a = make<at>(std::make_unique<int>(10), std::make_unique<int>(20));

    // build a vector, using make<> because an initializer_list requires a copyable type  
    auto v = make<vt>(std::make_unique<int>(10), std::make_unique<int>(20));
}
Richard Hodges
  • 64,204
  • 6
  • 75
  • 124
  • The question was if an `initializer_list` can be moved from, not whether anyone had workarounds. Besides, the main selling point of `initializer_list` is that it is only templated on the element type, not the number of elements, and therefore does not require recipients to also be templated - and this completely loses that. – underscore_d Jul 02 '17 at 14:11
  • 1
    @underscore_d you're absolutely right. I take the view that sharing knowledge related to the question is a good thing in itself. In this case, perhaps it helped the OP and perhaps it didn't - he didn't respond. More often than not however, the OP and others welcome extra material related to the question. – Richard Hodges Jul 03 '17 at 08:40
  • Sure, it may indeed help for readers who want something like `initializer_list` but aren't subject to all the constraints that make it useful. :) – underscore_d Jul 03 '17 at 10:25
  • @underscore_d which of the constraints have I overlooked? – Richard Hodges Jul 03 '17 at 13:07
  • All I mean is that `initializer_list` (via compiler magic) avoids having to template functions on the number of elements, something that is inherently required by alternatives based on arrays and/or variadic functions, thus constraining the range of cases where the latter are usable. By my understanding, this is precisely one of the main rationales for having `initializer_list`, so it seemed worth mentioning. – underscore_d Jul 03 '17 at 13:11
0

It seems not allowed in the current standard as already answered. Here is another workaround to achieve something similar, by defining the function as variadic instead of taking an initializer list.

#include <vector>
#include <utility>

// begin helper functions

template <typename T>
void add_to_vector(std::vector<T>* vec) {}

template <typename T, typename... Args>
void add_to_vector(std::vector<T>* vec, T&& car, Args&&... cdr) {
  vec->push_back(std::forward<T>(car));
  add_to_vector(vec, std::forward<Args>(cdr)...);
}

template <typename T, typename... Args>
std::vector<T> make_vector(Args&&... args) {
  std::vector<T> result;
  add_to_vector(&result, std::forward<Args>(args)...);
  return result;
}

// end helper functions

struct S {
  S(int) {}
  S(S&&) {}
};

void bar(S&& s) {}

template <typename T, typename... Args>
void foo(Args&&... args) {
  std::vector<T> args_vec = make_vector<T>(std::forward<Args>(args)...);
  for (auto& arg : args_vec) {
    bar(std::move(arg));
  }
}

int main() {
  foo<S>(S(1), S(2), S(3));
  return 0;
}

Variadic templates can handle r-value references appropriately, unlike initializer_list.

In this example code, I used a set of small helper functions to convert the variadic arguments into a vector, to make it similar to the original code. But of course you can write a recursive function with variadic templates directly instead.

Community
  • 1
  • 1
  • The question was if an `initializer_list` can be moved from, not whether anyone had workarounds. Besides, the main selling point of `initializer_list` is that it is only templated on the element type, not the number of elements, and therefore does not require recipients to also be templated - and this completely loses that. – underscore_d Jul 02 '17 at 14:11
0

I have a much simpler implementation that makes use of a wrapper class which acts as a tag to mark the intention of moving the elements. This is a compile-time cost.

The wrapper class is designed to be used in the way std::move is used, just replace std::move with move_wrapper, but this requires C++17. For older specs, you can use an additional builder method.

You'll need to write builder methods/constructors that accept wrapper classes inside initializer_list and move the elements accordingly.

If you need some elements to be copied instead of being moved, construct a copy before passing it to initializer_list.

The code should be self-documented.

#include <iostream>
#include <vector>
#include <initializer_list>

using namespace std;

template <typename T>
struct move_wrapper {
    T && t;

    move_wrapper(T && t) : t(move(t)) { // since it's just a wrapper for rvalues
    }

    explicit move_wrapper(T & t) : t(move(t)) { // acts as std::move
    }
};

struct Foo {
    int x;

    Foo(int x) : x(x) {
        cout << "Foo(" << x << ")\n";
    }

    Foo(Foo const & other) : x(other.x) {
        cout << "copy Foo(" << x << ")\n";
    }

    Foo(Foo && other) : x(other.x) {
        cout << "move Foo(" << x << ")\n";
    }
};

template <typename T>
struct Vec {
    vector<T> v;

    Vec(initializer_list<T> il) : v(il) {
    }

    Vec(initializer_list<move_wrapper<T>> il) {
        v.reserve(il.size());
        for (move_wrapper<T> const & w : il) {
            v.emplace_back(move(w.t));
        }
    }
};

int main() {
    Foo x{1}; // Foo(1)
    Foo y{2}; // Foo(2)

    Vec<Foo> v{Foo{3}, move_wrapper(x), Foo{y}}; // I want y to be copied
    // Foo(3)
    // copy Foo(2)
    // move Foo(3)
    // move Foo(1)
    // move Foo(2)
}
bumfo
  • 135
  • 2
  • 3
0

Instead of using a std::initializer_list<T>, you can declare your argument as an array rvalue reference:

template <typename T>
void bar(T &&value);

template <typename T, size_t N>
void foo(T (&&list)[N] ) {
   std::for_each(std::make_move_iterator(std::begin(list)),
                 std::make_move_iterator(std::end(list)),
                 &bar);
}

void baz() {
   foo({std::make_unique<int>(0), std::make_unique<int>(1)});
}

See example using std::unique_ptr<int>: https://gcc.godbolt.org/z/2uNxv6

Jorge Bellon
  • 2,369
  • 10
  • 22
-1

Consider the in<T> idiom described on cpptruths. The idea is to determine lvalue/rvalue at run-time and then call move or copy-construction. in<T> will detect rvalue/lvalue even though the standard interface provided by initializer_list is const reference.

Sumant
  • 4,166
  • 1
  • 21
  • 30
  • 4
    Why on earth would you want to determine the value category at runtime when the compiler already knows it? – fredoverflow Sep 18 '13 at 19:16
  • 1
    Please read the blog and leave me a comment if you disagree or have a better alternative. Even if the compiler knows the value category, initializer_list does not preserve it because it has only const iterators. So you need to "capture" the value category when you construct the initializer_list and pass it through so the function can make use of it as it pleases. – Sumant Sep 20 '13 at 20:40
  • 5
    This answer is basically useless without following the link, and SO answers should be useful without following links. – Yakk - Adam Nevraumont Jul 19 '16 at 08:19
  • 1
    @Sumant [copying my comment from an identical post elsewhere] Does that humungous mess actually provide any measurable benefits to performance or memory usage, and if so, a sufficiently large amount of such benefits to adequately offset how terrible it looks and the fact that it takes about an hour to figure out what it's trying to do? I kinda doubt it. – underscore_d Aug 09 '16 at 20:05