0

There are several questions (like this) in SO about the impossibility of moving objects out of an initializer list and how that may lead to excessive copies. This question is not about that.

My question is, given that an initializer list is basically a container of locally valid const references supporting shallow copy, why does it causes copies of its (lvalue) elements on creation? Rvalues in an initializer list are not copied, just moved, but lvalues are indeed copied. Here is an example that shows it:

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

struct A
{
    A()
    {
        std::cout << "Creating A" << std::endl;
    }

    ~A()
    {
        std::cout << "Deleting A" << std::endl;
    }

    A(const A &other)
    {
        std::cout << "Copying A" << std::endl;
    }

    A(A &&other)
    {
        std::cout << "Moving A" << std::endl;
    }

    A &operator=(const A &other)
    {
        std::cout << "Copy-assigning A" << std::endl;
        return *this;
    }

    A &operator=(A &&other)
    {
        std::cout << "Move-assigning A" << std::endl;
        return *this;
    }
};

int main(int, char **)
{
    A a;
    std::cout << "Creating initializer list 1." << std::endl;
    std::initializer_list<A> il1 { a };
    std::cout << "Created initializer list 1." << std::endl;
    std::vector<A> v {A(), A(), A()};
    std::cout << "Creating initializer list 2." << std::endl;
    std::initializer_list<std::vector<A>> il2 { v };
    std::cout << "Created initializer list 2." << std::endl;
    return 0;
}

And the output (with GCC 5.4 and Clang 3.8):

Creating A
Creating initializer list 1.
Copying A
Created initializer list 1.
Creating A
Creating A
Creating A
Copying A
Copying A
Copying A
Deleting A
Deleting A
Deleting A
Creating initializer list 2.
Copying A
Copying A
Copying A
Created initializer list 2.
Deleting A
Deleting A
Deleting A
Deleting A
Deleting A
Deleting A
Deleting A
Deleting A

The relevant parts are those between Creating... and Created..., where it can be seen that the initializer list produces copies of its arguments. Looking in cppreference, it says that (as part of C++14 specification) std::initializer_list<T> holds internally an array const T[N], which would explain the copies. I don't understand why this is necessary. As suggested in the comments, a workaround is to use an initializer list of pointers or std::reference_wrappers, but wouldn't it be better if the internal structure were itself an array of pointers or std::reference_wrappers? As it is right now, it looks as if initializer lists were meant to be used with rvalues (or light objects), but at the same time they do not allow to move objects out of them. Is there some case where not copying the lvalues to an internal structure could be harmful? Or is it just to avoid one pointer dereferencing operation?

jdehesa
  • 58,456
  • 7
  • 77
  • 121
  • Initializer_list has const object :( – Jarod42 Jul 19 '17 at 11:17
  • @underscore_d That question is about being able to move things _out_ of an initializer list, which I know is not possible (or not meant to be done at least). What I want to know is, given that, why does it require its inputs to be rvalues (or otherwise causes copies). – jdehesa Jul 19 '17 at 11:21
  • 1
    @jdehesa It is just an array abstracted behind a pair of iterators. So, just as you would have to copy/move lvalues into any other container, you have to copy/move them into an `initializer_list`. Or - of course - use an `initializer_list` of pointers or `reference_wrapper`s to the original objects that you don't want to copy/move. – underscore_d Jul 19 '17 at 11:22
  • @underscore_d Okay, but then, wouldn't it have made more sense that `initializer_list` _always_ used pointers or `reference_wrapper`s? Or is there some case were _not_ copying the object could cause issues? – jdehesa Jul 19 '17 at 11:27
  • Huh? That would seem to make it unusable for its primary motivation, which is to pass a variable-length list of arguments to container constructors, thus avoiding large stacks of `push|emplace_back()` commands. If the elements had to be mere references or pointers, then you would need to write a large stack of declarations _and then_ the `initializer_list` where you must create a reference to each one - the worst of both worlds. I suspect most cases already use rvalues when constructing the list itself, rather than passing in copies of existing objects, making the real behaviour benign. – underscore_d Jul 19 '17 at 11:31
  • 1
    In cases where I want to avoid the extra copies, I just hold my nose and call `emplace()` or `whateverFunction()` for each object manually. The goal of `initializer_list` is not to be all things to all people (as if that's possible); it's mostly to make construction of/insertion to containers nicer. – underscore_d Jul 19 '17 at 11:34
  • @underscore_d No, sorry, what I meant was, wouldn't it be better if the internal structure of an initializer list was a list of pointers or reference wrappers (not that the user of the list needed to create them, the initializer list would do it internally). – jdehesa Jul 19 '17 at 11:34
  • I dunno - but would assume/hope such questions were already discussed in the papers that led to the class being added to the Standard. – underscore_d Jul 19 '17 at 11:38
  • The design of `initializer_list` allows `std::initializer_list x = {1,2,3}; f(x); g(x);` to not produce any dangling pointer. But now this seems to be a rare use case in practice.... – cpplearner Jul 19 '17 at 12:03

0 Answers0