Recently, I had the following
struct data {
std::vector<int> V;
};
data get_vector(int n)
{
std::vector<int> V(n,0);
return {V};
}
The problem with this code is that when the struct is created a copy occurs and the solution is instead to write return {std::move(V)}
Are there linter or code analyzer that would detect such spurious copy operations? Neither cppcheck, cpplint, nor clang-tidy can do it.
EDIT: Several points to make my question clearer:
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.
I know that a copy operation occurred because I used compiler explorer and it shows a call to memcpy.
Did you put your complete application into the compiler explorer, and did you enable optimizations? If not, then what you saw in the compiler explorer might or might not be what is happening with your application.
One issue with the code you posted is that you first create a std::vector
, and then copy it into an instance of data
. It would be better to initialize data
with the vector:
data get_vector(int n)
{
return {std::vector<int> V(n,0)};
}
Also, if you just give the compiler explorer the definition of data
and get_vector()
, and nothing else, it has to expect the worse. If you actually give it some source code that uses get_vector()
, then look at what assembly is generated for that source code. See this example for what the above modification plus actual usage plus compiler optimizations can cause the compiler to produce.
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