Why pass by value and not by const reference?

2020-06-12 06:05发布

问题:

Since const reference is pretty much the same as passing by value but without creating a copy (to my understanding). So is there a case where it is needed to create a copy of the variables (so we would need to use pass by value).

回答1:

There are situations where you don't modify the input, but you still need an internal copy of the input, and then you may as well take the arguments by value. For example, suppose you have a function that returns a sorted copy of a vector:

template <typename V> V sorted_copy_1(V const & v)
{
    V v_copy = v;
    std::sort(v_copy.begin(), v_copy.end());
    return v;
}

This is fine, but if the user has a vector that they never need for any other purpose, then you have to make a mandatory copy here that may be unnecessary. So just take the argument by value:

template <typename V> V sorted_copy_2(V v)
{
    std::sort(v.begin(), v.end());
    return v;
}

Now the entire process of producing, sorting and returning a vector can be done essentially "in-place".

Less expensive examples are algorithms which consume counters or iterators which need to be modified in the process of the algorithm. Again, taking those by value allows you to use the function parameter directly, rather than requiring a local copy.



回答2:

  1. It's usually faster to pass basic data types such as ints, floats and pointers by value.
  2. Your function may want to modify the parameter locally, without altering the state of the variable passed in.
  3. C++11 introduces move semantics. To move an object into a function parameter, its type cannot be const reference.


回答3:

Like so many things, it's a balance.

We pass by const reference to avoid making a copy of the object.

When you pass a const reference, you pass a pointer (references are pointers with extra sugar to make them taste less bitter). And assuming the object is trivial to copy, of course.

To access a reference, the compiler will have to dereference the pointer to get to the content [assuming it can't be inlined and the compiler optimises away the dereference, but in that case, it will also optimise away the extra copy, so there's no loss from passing by value either].

So, if your copy is "cheaper" than the sum of dereferencing and passing the pointer, then you "win" when you pass by value.

And of course, if you are going to make a copy ANYWAY, then you may just as well make the copy when constructing the argument, rather than copying explicitly later.



回答4:

The best example is probably the Copy and Swap idiom:

C& operator=(C other)
{
    swap(*this, other);
    return *this;
} 

Taking other by value instead of by const reference makes it much easier to write a correct assignment operator that avoids code duplication and provides a strong exception guarantee!

Also passing iterators and pointers is done by value since it makes those algorithms much more reasonable to code, since they can modify their parameters locally. Otherwise something like std::partition would have to immediately copy its input anyway, which is both inefficient and looks silly. And we all know that avoiding silly-looking code is the number one priority:

template<class BidirIt, class UnaryPredicate>
BidirIt partition(BidirIt first, BidirIt last, UnaryPredicate p)
{
    while (1) {
        while ((first != last) && p(*first)) {
            ++first;
        }
        if (first == last--) break;
        while ((first != last) && !p(*last)) {
            --last;
        }
        if (first == last) break;
        std::iter_swap(first++, last);
    }
    return first;
}


回答5:

A const& cannot be changed without a const_cast through the reference, but it can be changed. At any point where code leaves the "analysis range" of your compiler (maybe a function call to a different compilation unit, or through a function pointer it cannot determine the value of at compilation time) it must assume that the value referred to may have changed.

This costs optimization. And it can make it harder to reason about possible bugs or quirks in your code: a reference is non-local state, and functions that operate only on local state and produce no side effects are really easy to reason about. Making your code easy to reason about is a large boon: more time is spent maintaining and fixing code than writing it, and effort spent on performance is fungible (you can spent it where it matters, instead of wasting time on micro optimizations everywhere).

On the other hand, a value requires that the value be copied into local automatic storage, which has costs.

But if your object is cheap to copy, and you don't want the above effect to occur, always take by value as it makes the compilers job of understanding the function easier.

Naturally only when the value is cheap to copy. If expensive to copy, or even if the copy cost is unknown, that cost should be enough to take by const&.

The short version of the above: taking by value makes it easier for you and the compiler to reason about the state of the parameter.

There is another reason. If your object is cheap to move, and you are going to store a local copy anyhow, taking by value opens up efficiencies. If you take a std::string by const&, then make a local copy, one std::string may be created in order to pass thes parameter, and another created for the local copy.

If you took the std::string by value, only one copy will be created (and possibly moved).

For a concrete example:

std::string some_external_state;
void foo( std::string const& str ) {
  some_external_state = str;
}
void bar( std::string str ) {
  some_external_state = std::move(str);
}

then we can compare:

int main() {
  foo("Hello world!");
  bar("Goodbye cruel world.");
}

the call to foo creates a std::string containing "Hello world!". It is then copied again into the some_external_state. 2 copies are made, 1 string discarded.

The call to bar directly creates the std::string parameter. Its state is then moved into some_external_state. 1 copy created, 1 move, 1 (empty) string discarded.

There are also certain exception safety improvements caused by this technique, as any allocation happens outside of bar, while foo could throw a resource exhausted exception.

This only applies when perfect forwarding would be annoying or fail, when moving is known to be cheap, when copying could be expensive, and when you know you are almost certainly going to make a local copy of the parameter.

Finally, there are some small types (like int) which the non-optimized ABI for direct copies is faster than the non-optimized ABI for const& parameters. This mainly matters when coding interfaces that cannot or will not be optimized, and is usually a micro optimization.