std::vector init with braces call copy constructor twice
Asked Answered
S

3

6

Why when I init std::vector with braces

std::vector<TS> vec {ts1, ts2};

Compiler call twice copy constructor operator? On the other hand - with push_back it called only once.

#include <iostream>
#include <vector>
using namespace std;

struct TS{
    TS(){
        cout<<"default constructor\n";
    }

    TS(const TS &other) {
        cout<<"Copy constructor\n";
    }

    TS(TS &&other) noexcept{
        cout<<"Move constructor\n";
    }

    TS& operator=(TS const& other)
    {
        cout<<"Copy assigment\n";
        return *this;
    }

    TS& operator=(TS const&& other) noexcept
    {
        cout<<"Move assigment\n";
        return *this;
    }

    ~TS(){
        cout<<"destructor\n";
    }

};

int main() {
    TS ts1;
    TS ts2;
    cout<<"-----------------------------------------\n";
    std::vector<TS> vec {ts1, ts2};
    //vec.push_back(ts1);
    //vec = {ts1, ts2};
    cout<<"-----------------------------------------\n";



    return 0;
}

http://ideone.com/qcPG7X

Sonni answered 10/12, 2013 at 17:48 Comment(11)
No assignment operator should be called with brace-init, but the arguments are copied into the std::initializer_list<TS> and copied out => two copy-constructor calls per argument. You can move them into the initializer_list (std::vector<TS> vec {std::move(ts1), std::move(ts2)};) but you cannot move them out, so at least one copy-constructor call is necessary.Escutcheon
Can I, at least force it somehow to use move assigment, instead of cope?Sonni
Not with list-initialization. You can however write a workarounds, for example a function that returns a type that stores the arguments of the function as references, and which provides a conversion to a std::vector, called à la vector<TS> vec( collect_references(ts1, ts2) );.Escutcheon
@DyP function should return std::vector. Ok, good idea, thank you.Sonni
No, because it wouldn't now what value_type. It should return a type with a template<class T> operator std::vector<T>() const to deduce the value_type for the vector to be initialized; it then can use emplace_back and avoid any copy-ctor, if possible. Live exampleEscutcheon
Oops, I forgot to std::forward appropriately ;) Fixed live exampleEscutcheon
@DyP What does this means auto collect_references(Ts&&... pp) -> reference_collector<Ts&&...> just type deduction? And you make "new" with this "std::forward<Ts>(pp)" ? Honestly, a little bit complicate to me.Sonni
auto function_name(parameter_list) -> return_type is just another way to write return_type function_name(parameter_list) (where you can use the parameter names in the return type); the Ts&&... here declares a function parameter pack, where each parameter is a "universal reference". I agree that it's complicated, but it should be invisible to the user of collect_references. Each argument you pass is just forwarded to a vec.emplace_back call without any conversion, copying or moving.Escutcheon
But to avoid copying we should do std::move on each argument of collect_references?Sonni
As can be seen in my fixed example, you can use a std::move to store an rvalue reference in the reference collector; the argument will then be moved exactly once (into the vector) and never copied. If you pass an lvalue, the argument will be copied exactly once (into the vector).Escutcheon
I mean move std::move to the function/template/whatever body. That make it more readable. But this is seems to be impossible.Sonni
B
5

From what I understand, initializer_lists pass everything by const-reference. It is probably not safe to move from one. The initializer_list constructor of a vector will copy each of the elements.

Here are some links: initializer_list and move semantics

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.

Is it safe to move elements of a initializer list?

initializer_list only provides const access to its elements. You could use const_cast to make that code compile, but then the moves might end up with undefined behaviour (if the elements of the initializer_list are truly const). So, no it is not safe to do this moving. There are workarounds for this, if you truly need it.

Can I list-initialize a vector of move-only type?

The synopsis of in 18.9 makes it reasonably clear that elements of an initializer list are always passed via const-reference. Unfortunately, there does not appear to be any way of using move-semantic in initializer list elements in the current revision of the language.

questions regarding the design of std::initializer_list

From section 18.9 of the C++ Standard:

An object of type initializer_list provides access to an array of objects of type const E. [ Note: A pair of pointers or a pointer plus a length would be obvious representations for initializer_list. initializer_list is used to implement initializer lists as specified in 8.5.4. Copying an initializer list does not copy the underlying elements. — end note ]

I think the reason for most of these things is that std::initializer_list isn't actually a container. It doesn't have value semantics, it has pointer semantics. Which is made obvious by the last portion of the quote: Copying an initializer list does not copy the underlying elements. Seeing as they were intended solely for the purpose of initializing things, I don't think it's that surprising that you don't get all the niceties of more robust containers such as tuples.

If I understand the last part correctly, it means that two sets of copies are needed since initializer_list does not copy the underlying elements. (The previous quote is only relevant if you attempt to use an initializer_list without copying out the elements.)

What is the underlying structure of std::initializer_list?

No, you can't move from the elements of an initializer_list, since elements of an initializer_list are supposed to be immutable (see the first sentence of the paragraph quoted above). That's also the reason why only const-qualified member functions give you access to the elements.


If you want, you can use emplace_back:

vec.emplace_back(TS());
vec.emplace_back(TS());
vec.push_back(std::move(ts1));
vec.push_back(std::move(ts2));
Bloodyminded answered 10/12, 2013 at 18:4 Comment(1)
I can use emplace_back, indeed. But with {} - it looks much beautifull.Sonni
K
1

Because you have two elements there.

Kanchenjunga answered 10/12, 2013 at 18:23 Comment(3)
The copy-ctor will be called twice for each argument, so four times in total for the two arguments.Escutcheon
Poorly stated question, thenKanchenjunga
The OP even says "copy assignment operator" ;)Escutcheon
C
0

In the code you have provided, with Microsoft compiler, the copy constructor for TS when it is in the vector initializer list will be called only once!

TS ts1;  // contructor called
std::vector<TS> vec {ts1};  // copy constructor called. Only once.

If you create an object in the initializer list, it will do both:

std::vector<TS> vec {TS()}; // constructor, then copy constructor called

In the second case, if you have a number of objects, all constructors will be called first, then all copy contructors.

Carinacarinate answered 16/3 at 1:25 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.