class Foo {
std::vector<SomeType> data_;
};
Say Foo
can only be constructed by make a copy (technically I mean a copy or move) of a std::vector<SomeType>
object. What's the best way to write constructor(s) for Foo
?
My first feeling is
Foo(std::vector<SomeType> data) noexcept : data_(std::move(data)) {};
Using it, construction of an instance takes 0 or 1 times of vector copy, depending on whether the argument for {data} is moveable or not.
Your first feeling is good. Strictly speaking it is not optimal. But it is so close to optimal that you would be justified in saying you don't care.
Explanation:
Foo(std::vector<SomeType> data) noexcept : data_(std::move(data)) {};
When the client passes in an lvalue std::vector<SomeType>
1 copy will be made to bind to the data
argument. And then 1 move will be made to "copy" the argument into data_
.
When the client passes in an xvalue std::vector<SomeType>
1 move will be made to bind to the data
argument. And then another move will be made to "copy" the argument into data_
.
When the client passes in a prvalue std::vector<SomeType>
the move will be elided in binding to the data
argument. And then 1 move will be made to "copy" the argument into data_
.
Summary:
client argument number of copies number of moves
lvalue 1 1
xvalue 0 2
prvalue 0 1
If you instead did:
Foo(const std::vector<SomeType>& data) : data_(data) {};
Foo( std::vector<SomeType>&& data) noexcept : data_(std::move(data)) {};
Then you have a very slightly higher performance:
When the client passes in an lvalue std::vector<SomeType>
1 copy will be made to copy the argument into data_
.
When the client passes in an xvalue std::vector<SomeType>
1 move will be made to "copy" the argument into data_
.
When the client passes in a prvalue std::vector<SomeType>
1 move will be made to "copy" the argument into data_
.
Summary:
client argument number of copies number of moves
lvalue 1 0
xvalue 0 1
prvalue 0 1
Conclusion:
std::vector
move constructions are very cheap, especially measured with respect to copies.
The first solution will cost you an extra move when the client passes in an lvalue. This is likely to be in the noise level, compared to the cost of the copy which must allocate memory.
The first solution will cost you an extra move when the client passes in an xvalue. This could be a weakness in the solution, as it doubles the cost. Performance testing is the only reliable way to assure that either this is, or is not an issue.
Both solutions are equivalent when the client passes a prvalue.
As the number of parameters in the constructor increases, the maintenance cost of the second solution increases exponentially. That is you need every combination of const lvalue and rvalue for each parameters. This is very manageable at 1 parameter (two constructors), less so at 2 parameters (4 constructors), and rapidly becomes unmanageable after that (8 constructors with 3 parameters). So optimal performance is not the only concern here.
If one has many parameters, and is concerned about the cost of an extra move construction for lvalue and xvalue arguments, there are other solutions, but they involve relatively ugly template meta-programming techniques which many consider too ugly to use (I don't, but I'm trying to be unbiased).
For std::vector
, the cost of an extra move construction is typically small enough you won't be able to measure it in overall application performance.
The complexity problem of the performance optimal solution mentioned in Howard Hinnant's answer for constructors taking multiple arguments would be solved by use of perfect forwarding:
template<typename A0>
Foo(A0 && a0) : data_(std::forward<A0>(a0)) {}
In case of more parameters, extend accordingly:
template<typename A0, typename A1, ...>
Foo(A0 && a0, A1 && a1, ...)
: m0(std::forward<A0>(a0))
, m1(std::forward<A1>(a1))
, ...
{}
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With