Reputation: 12332
I have this little test code to show what operations std::vector does on a class A
for various operations. I'm a bit disappointed by how many copy operations are done.
Update:
noexcept
in the move constructor. fixed.std::initializer
lists can't be moved. So I extended std::vector
and used template parameter pack to move elements where std::initializer
lists is used.Code looks like this now: https://godbolt.org/z/5xc9sM6r7
#include <iostream>
#include <vector>
#include <source_location>
#define LOG std::cout << "@" << this << ": \t" << (this->moved ? "moved" : "full") << "\t" << std::source_location::current().function_name() << std::endl
#define DO(x) std::cout << std::endl << "##### "#x" #####" << std::endl; x
template <typename T>
class vector : public std::vector<T> {
public:
vector() : std::vector<T>() { }
template <typename... U>
vector(U &&...u) {
std::cout << "parameter pack" << std::endl;
std::vector<T>::reserve(sizeof...(u));
([this](auto &&x){ std::vector<T>::emplace_back(std::move(x));}(u), ...);
}
// can't delete this or the above doesn't work
// template <typename U>
// vector(std::initializer_list<U>) = delete;
};
struct A {
A(int) { LOG; }
A(const A &) { LOG; }
A(A &&other) noexcept { LOG; other.moved = true; }
A & operator=(const A &) { LOG; return *this; }
A & operator=(A &&other) noexcept { LOG; other.moved = true; return *this; }
~A() { LOG; }
bool moved{false};
};
int main() {
DO(A a{A{A{A{A{A{1}}}}}});
DO(vector<A> v{A{1}});
DO(v.reserve(4));
DO(v.emplace_back(1));
DO(v.emplace_back(A{1}));
DO({A a{1}; v.back() = a; });
DO(v.back() = A{1});
DO(return 0);
}
Output looks like this: https://godbolt.org/z/8YoM6aMGo
##### A a{A{A{A{A{A{1}}}}}} #####
@0x7ffc8a13ef1b: full A::A(int)
This is perfect, all the copies are elided and the object is constructed directly in-place where it belongs.
##### vector<A> v{A{1}} #####
@0x7fff4872de5c: full A::A(int)
parameter pack
@0xa3eec0: full A::A(A&&)
@0x7fff4872de5c: moved A::~A()
Still wish this would elide the copy. I'm thinking of this as ((A*)0x18ffec0)->A{A{1}}
, loosely speaking.
This still seems to go through an std::initializer_list
in some form. It fails if I delete that constructor in vector
.
##### v.reserve(4) #####
@0xa3eee0: full A::A(A&&)
@0xa3eec0: moved A::~A()
This didn't move before because of missing noexcept
.
##### v.emplace_back(1) #####
@0xa3eee1: full A::A(int)
Finally something works out, a nice in-place construction.
##### v.emplace_back(A{1}) #####
@0x7fff4872de5c: full A::A(int)
@0xa3eee2: full A::A(A&&)
@0x7fff4872de5c: moved A::~A()
Again with the construct + move. Why not construct the A
at 0x18ffee2? Oddly enough this doesn't seem to suffer from an std::initializer_list
breaking moves.
But couldn't this be made to construct in-place?
##### {A a{1}; v.back() = a; } #####
@0x7ffc8a13ef1c: full A::A(int)
@0x18ffee2: full A& A::operator=(const A&)
@0x7ffc8a13ef1c: full A::~A()
Nothing there the vector can do, must copy.
##### v.back() = A{1} #####
@0x7ffc8a13ef1c: full A::A(int)
@0x18ffee2: full A& A::operator=(A&&)
@0x7ffc8a13ef1c: moved A::~A()
More move semantic.
##### return 0 #####
@0x18ffee0: full A::~A()
@0x18ffee1: full A::~A()
@0x18ffee2: full A::~A()
@0x7ffc8a13ef1b: full A::~A()
Destruct of the vector and local variable.
Update 2: Using (...)
instead of `{...}``to initialize skips the initializer list and goes straight to the template pack (as you can check if you delete the initializer list constructor):
##### vector<A> u(1,2,3,4) #####
parameter pack
@0x205dec0: full A::A(int)
@0x205dec1: full A::A(int)
@0x205dec2: full A::A(int)
@0x205dec3: full A::A(int)
##### vector<A> v(A{1}) #####
@0x7ffcd497202c: full A::A(int)
parameter pack
@0x205dee0: full A::A(A&&)
@0x7ffcd497202c: moved A::~A()
Doesn't help to achieve in-place construction though when a temporary object is given as argument.
Update 3: If you strip away all the in between layers and container logic I'm left with this simple bit of code:
A *p = static_cast<A*>(operator new[](sizeof(A), static_cast<std::align_val_t>(alignof(A))));
std::construct_at(p, A{1});
This gives the following output:
##### std::construct_at(p, A{1}) #####
@0x7ffe94fe5220: full A::A(int)
@0xa70eb0: full A::A(A&&)
@0x7ffe94fe5220: moved A::~A()
std::construct_at
calls the move constructor for A a the given address. So it's just doing A{A{1}}
except the address of the outer A
is given as argument instead of some place on the stack the compiler choose.
So I'm really asking why std::construct_at(p, A{1})
and A{A{1}}
aren't optimized the same way. Does the standard maybe already allow optimizing it? Or can we change the code or standard so it does optimize the same way?
Upvotes: 1
Views: 537
Reputation: 118352
std::vector<A> v{A{1}}
This invokes the constructor that takes a std::initializer_list
as a parameter. std::initializer_list
provides access only to const
values in the initializer list, which can't be moved.
v.reserve(4)
No moving here because your move constructor is not noexcept
. You only declared your move-assignment operator as noexcept
but you forgot about the move constructor. vector
also requires a noexcept
move constructor before it will employ move semantics when reallocating.
Declare your move constructor as noexcept
and this instance, and the remaining ones that involve reallocation, should now employ move semantics.
Upvotes: 0