Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Best way to write constructor of a class who holds a STL container in C++11

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.

like image 341
updogliu Avatar asked Feb 23 '14 01:02

updogliu


2 Answers

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.

like image 126
Howard Hinnant Avatar answered Mar 06 '23 02:03

Howard Hinnant


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))
 , ...
 {}
like image 20
user3344202 Avatar answered Mar 06 '23 02:03

user3344202