c++ – Pass by reference and then copy vs pass by value

Question:

There is a certain type that is difficult to copy, for example:

struct S 
{ 
    int a[100]; 
};

And the task is to process the value of a variable of this type with the return of a modified copy, i.e. the original must be retained. Two approaches suggest themselves:

  1. S test(const S& s) { S news = s; // делаем копию news.a[42] = 100500; // изменяем return news; // возвращаем }
  2.  S test(S s) // делаем копию { sa[42] = 100500; // изменяем return s; // возвращаем }

Option 2 looks shorter in terms of code, however, as the assembly shows, passing by reference yields shorter assembly code.

Why is this happening and what are the other pros and cons of these two approaches in order to understand which one to prefer? Maybe there are some other options?

Answer:

In modern C ++, it is the second option that is considered more preferable. Those. if you know you will need a copy anyway, it is better for the compiler to make the copy for you, not yourself.

However, the traditional rationale given for this statement relies on types that are "heavy" to copy, not because they are large in themselves, but because they require deep copying. Those. we are talking about types that are compact at the shallow level, but own additional resources through pointers / handles. The whole idea here is that the compiler will be able to replace copying with moving in situations where the original value is temporary / relocatable.

For example, if in your case you replace S with std::string , then when you call test("abc") second option, when preparing the arguments, will do without deep copying at all, while in the first option you will certainly do deep copying yourself.

(A variant with two separate functions may be even more efficient – for the const std::string & parameter and for the std::string && parameter, but if you are not trying to squeeze out the last processor clock cycles, then one function with the std::string parameter often looks like more attractive.)

In the case when the "weight" of an object is built directly into the object itself, as in your example, you will not be able to save on copying. I would expect the same performance from both options.

Some nuance is that, according to the abstract semantics of the language, the creation of a copy in the second version is done in the context of the calling code. Some implementations take this semantics literally — they create and reserve a copy in the context of the calling code. With this, memory can be reserved in advance, regardless of whether the function is actually called during execution. Those. by writing, say, a recursive function like this

void recursive(unsigned n, const S &s)
{
  if (n > 0)
    recursive(n - 1, s);
  else
    test(s);
}

you may be surprised to find that when you use the second variant of the test function, the memory on the stack for the copy s is allocated at each level of recursion, when in fact this memory is needed only at the very bottom of the recursion. The first test option will be free of this drawback.

Other implementations can do it more economically: even when using the second test option, reserve memory only if the function is actually called.

Scroll to Top