There are some interesting bits about the C++ runtime. Consider this
problem: you want to add an element to a vector of type
std::vector<std::pair<int, Foo>>
with as little
overhead as possible. That means one construction – no extra moves, no
extra copies, nothing.
For this problem, let us use this implementation of Foo:
class Foo {
public:
Foo() {
std::cout << "constructor\n";
}
Foo(int x) {
std::cout << "constructor2\n";
}
Foo(const Foo& f) {
std::cout << "copy constructor\n";
}
Foo(Foo&& f) {
std::cout << "move constructor\n";
}
};
Obviously we want to use emplace_back
to elide the move
you would traditionally get from something like
vec.push_back(std::move(f))
.
When run:
$ ./a.out
constructor
move constructor
So what happened? Clearly Foo
was
std::move
d at least once. In fact, it happens when
emplace_back
is evaluated, since we constructed an rvalue
Foo{}
.
Ok, so we need a different approach. How about forwarding through the
arguments to Foo
, so Foo
is constructed as
late as possible?
#include <tuple>
int main()
{
std::vector<std::pair<int, Foo>> list;
list.emplace_back(std::piecewise_construct, std::forward_as_tuple(3), std::forward_as_tuple());
}
$ ./a.out
constructor
Here we use std::forward_as_tuple
to tell the compiler
that we want to call the 0-arg constructor. If using fancy C++ standard
library features isn’t your cup of tea, you could alternatively use a
dummy constructor:
$ ./a.out
constructor2
Great! So we solved our original problem. Or did we? Consider this:
int main()
{
std::vector<std::pair<int, Foo>> list;
list.emplace_back(3, 1);
std::cout << "---\n";
list.emplace_back(3, Foo{});
std::cout << "---\n";
list.emplace_back(3, 1);
}
$ ./a.out
constructor2
---
constructor
move constructor
copy constructor
---
constructor2
copy constructor
copy constructor
Woah, what’s with all the extra copies? Well, as it turns out, when a vector needs to be resized, enough contiguous memory for all elements needs to be allocated. Then, the contents of the old vector need to be move or copy constructed into the new memory. To elide this issue, we can preallocate memory for our vector.
int main()
{
std::vector<std::pair<int, Foo>> list;
list.reserve(3);
list.emplace_back(3, 1);
std::cout << "---\n";
list.emplace_back(3, Foo{});
std::cout << "---\n";
list.emplace_back(3, 1);
}
$ ./a.out
constructor2
---
constructor
move constructor
---
constructor2
Hey, now this is Looking good. But wait! There’s one outstanding
question: what’s with all the copies in the previous snippet? Shouldn’t
they be moves? We have a move constructor defined for
Foo
.
Well, it turns out due to exception-safe guarantees, C++ standard
collections will not use non-exception-safe move constructors. So how do
we fix this? Add noexcept
to the move constructor, like
so:
Then run the example without the list.reserve(3)
again:
$ ./a.out
constructor2
---
constructor
move constructor
move constructor
---
constructor2
move constructor
move constructor
Congratulations, now you know more about std::vector
than you wanted to know.