I believe you have the correct observation but the wrong interpretation!
The copy will not occur by returning the value, because every normal clever compiler will use (N)RVO in this case. From C++17 this is mandatory, so you can't see any copy by returning a local generated vector from the function.
OK, lets play a bit with std::vector
and what will happen during the construction or by filling it step by step.
First of all, lets generate a data type which makes every copy or move visible like this one:
template <typename DATA >
struct VisibleCopy
{
private:
DATA data;
public:
VisibleCopy( const DATA& data_ ): data{ data_ }
{
std::cout << "Construct " << data << std::endl;
}
VisibleCopy( const VisibleCopy& other ): data{ other.data }
{
std::cout << "Copy " << data << std::endl;
}
VisibleCopy( VisibleCopy&& other ) noexcept : data{ std::move(other.data) }
{
std::cout << "Move " << data << std::endl;
}
VisibleCopy& operator=( const VisibleCopy& other )
{
data = other.data;
std::cout << "copy assign " << data << std::endl;
}
VisibleCopy& operator=( VisibleCopy&& other ) noexcept
{
data = std::move( other.data );
std::cout << "move assign " << data << std::endl;
}
DATA Get() const { return data; }
};
And now lets start some experiments:
using T = std::vector< VisibleCopy<int> >;
T Get1()
{
std::cout << "Start init" << std::endl;
std::vector< VisibleCopy<int> > vec{ 1,2,3,4 };
std::cout << "End init" << std::endl;
return vec;
}
T Get2()
{
std::cout << "Start init" << std::endl;
std::vector< VisibleCopy<int> > vec(4,0);
std::cout << "End init" << std::endl;
return vec;
}
T Get3()
{
std::cout << "Start init" << std::endl;
std::vector< VisibleCopy<int> > vec;
vec.emplace_back(1);
vec.emplace_back(2);
vec.emplace_back(3);
vec.emplace_back(4);
std::cout << "End init" << std::endl;
return vec;
}
T Get4()
{
std::cout << "Start init" << std::endl;
std::vector< VisibleCopy<int> > vec;
vec.reserve(4);
vec.emplace_back(1);
vec.emplace_back(2);
vec.emplace_back(3);
vec.emplace_back(4);
std::cout << "End init" << std::endl;
return vec;
}
int main()
{
auto vec1 = Get1();
auto vec2 = Get2();
auto vec3 = Get3();
auto vec4 = Get4();
// All data as expected? Lets check:
for ( auto& el: vec1 ) { std::cout << el.Get() << std::endl; }
for ( auto& el: vec2 ) { std::cout << el.Get() << std::endl; }
for ( auto& el: vec3 ) { std::cout << el.Get() << std::endl; }
for ( auto& el: vec4 ) { std::cout << el.Get() << std::endl; }
}
What can we observe:
Example 1)
We create a vector from a initializer list and maybe we expect that we will see 4 times construct and 4 moves. But we get 4 copies! That sounds a bit mysterious, but the reason is the implementation of initializer list! Simply it is not allowed to move from the list as the iterator from the list is a const T*
which makes it impossible to move elements from it. A detailed answer on this topic can be found here: initializer_list and move semantics
Example 2)
In this case, we get a initial construction and 4 copies of the value. That is nothing special and is what we can expect.
Example 3)
Also here, we the the construction and some moves as expected. With my stl implementation the vector grows by factor 2 every time. So we see a first construct, another one and because the vector resizes from 1 to 2, we see the move of the first element. While adding the 3 one, we see a resize from 2 to 4 which needs a move of the first two elements. All as expected!
Example 4)
Now we reserve space and fill later. Now we have no copy and no move anymore!
In all cases, we do not see any move nor copy by returning the vector back to the caller at all! (N)RVO is taking place and no further action is required in this step!
Back to your question:
"How to find C++ spurious copy operations"
As seen above, you may introduce a proxy class in between for debugging purpose.
Making the copy-ctor private may not work in many cases, as you may have some wanted copies and some hidden ones. As above, only the code for example 4 will work with a private copy-ctor! And I can not answer the question, if the example 4 is the fastest one, as we fill peace by peace.
Sorry that I can not offer a general solution for finding "unwanted" copies here. Even if you dig your code for calls of memcpy
, you will not find all as also memcpy
will be optimized away and you see directly some assembler instructions doing the job without a call to your library memcpy
function.
My hint is not to focus on such a minor problem. If you have real performance issues, take a profiler and measure. There are so many potential performance killers, that investing much time on spurious memcpy
usage seems not such a worthwhile idea.
std::vector
by any means is not being what it purports to be. Your example shows an explicit copy, and it is only natural, and the correct approach, (again imho) to apply thestd::move
function as you suggest yourself if a copy is not what you want. Note that some compilers may omit the copying if optimizations flags are turned on, and the vector is unchanged. – Dihybrid