Home > Mobile >  Is it possible to avoid a copy when returning an argument from a function?
Is it possible to avoid a copy when returning an argument from a function?

Time:06-21

Suppose I have value type with some in-place operation. For example, something like this:

using MyType = std::array<100, int>;

void Reverse(MyType &value) {
  std::reverse(value.begin(), value.end());
}

(The type and operation can be more complicated, but the point is the operation works in-place and the type is trivially copyable and trivially destructible. Note that MyType is large enough to care about avoiding copies, but small enough that it probably doesn't make sense to allocate on the heap, and since it contains only primitives, it doesn't benefit from move semantics.)

I usually find it helpful to also define a helper function that doesn't change the value in-place, but returns a copy with the operation applied to it. Among other things, that enables code like this:

MyType value = Reversed(SomeFunction());

Considering that Reverse() operates in-place, it should be logically possible to calculate value without copying the result from SomeFunction(). How can I implement Reversed() to avoid unnecessary copies? I'm willing to define Reversed() as an inline function in a header if that's what's necessary to enable this optimization.

I can think of two ways to implement this:

inline MyType Reversed1(const MyType &value) {
  MyType result = value;
  Reverse(result);
  return result;
}

This benefits from return-value optimization but only after the argument value has been copied to result.

inline MyType Reversed2(MyType value) {
  Reverse(value);
  return value;
}

This might require the caller to copy the argument, except if it's already an rvalue, but I don't think return-value optimization is enabled this way (or is it?) so there's a copy upon return.

Is there a way to implemented Reversed() that avoids copies, ideally in a way that it's guaranteed by recent C standards?

CodePudding user response:

If you do want to reverse the string in-place so that the change to the string you send in as an argument is visible at the call site and you also want to return it by value, you have no option but to copy it. They are two separate instances.


One alternative: Return the input value by reference. It'll then reference the same object that you sent in to the function:

MyType& Reverse(MyType& value) {   // doesn't work with r-values
    std::reverse(std::begin(value), std::end(value));
    return value;
}

MyType Reverse(MyType&& value) {   // r-value, return a copy
    std::reverse(std::begin(value), std::end(value));
    return value;                  // no point in moving a std::array
}

Another alternative: Create the object you return in-place. You'll then return a separate instance with RVO in effect. No moves or copies. It'll be a separate instance from the one you sent in to the function though.

MyType Reverse(const MyType& value) {
    // Doesn't work with `std::array`s:
    return {std::rbegin(value), std::rend(value)}; 
}

The second alternative would work if std::array could be constructed from iterators like most other containers, but they can't. One solution could be to create a helper to make sure RVO works:

using MyType = std::array<int, 26>;

namespace detail {
    template<size_t... I>
    constexpr MyType RevHelper(const MyType& value, std::index_sequence<I...>) {
        // construct the array in reverse in-place:
        return {value[sizeof...(I) - I - 1]...};    // RVO
    }
} // namespace detail

constexpr MyType Reverse(const MyType& value) {
    // get size() of array in a constexpr fashion:
    constexpr size_t asize = std::tuple_size_v<MyType>;

    // RVO:
    return detail::RevHelper(value, std::make_index_sequence<asize>{});
}

CodePudding user response:

Your last option is the way to go (except for the typo):

MyType Reversed2(MyType value)
{
    Reverse(value);
    return value;
}

[N]RVO doesn't apply to return result;, but at least it's implicitly moved, rather than copied.

You'll have either a copy a move, or two moves, depending on the value category of the argument.

CodePudding user response:

You can use a helper method that turns your in-place operation into something that can work on Rvalues. When I tested this in GCC, it results in one move operation, but no copies. The pattern looks like this:

void Reversed(MyType & m);

MyType Reversed(MyType && m) {
  Reversed(m);
  return std::move(m);
}

Here is the full code I used to test whether this pattern results in copies or not:

#include <stdio.h>
#include <string.h>
#include <utility>

struct MyType {
  int * contents;

  MyType(int value0) {
    contents = new int[42];
    memset(contents, 0, sizeof(int) * 42);
    contents[0] = value0;
    printf("Created %p\n", this);
  }

  MyType(const MyType & other) {
    contents = new int[42];
    memcpy(contents, other.contents, sizeof(int) * 42);
    printf("Copied from %p to %p\n", &other, this);
  }

  MyType(MyType && other) {
    contents = other.contents;
    other.contents = nullptr;
    printf("Moved from %p to %p\n", &other, this);
  }

  ~MyType() {
    if (contents) { delete[] contents; }
  }
};

void Reversed(MyType & m) {
  for (int i = 0; i < 21; i  ) {
    std::swap(m.contents[i], m.contents[41 - i]);
  }
}

MyType Reversed(MyType && m) {
  Reversed(m);
  return std::move(m);
}

MyType SomeFunction() {
  return MyType(7);
}

int main() {
  printf("In-place modification\n");
  MyType x = SomeFunction();
  Reversed(x);
  printf("%d\n", x.contents[41]);

  printf("RValue modification\n");
  MyType y = Reversed(SomeFunction());
  printf("%d\n", y.contents[41]);
}

I'm not sure if this lack of copies is guaranteed by the standard, but I would think so, because there are some objects that are not copyable.

Note: The original question was just about how to avoid copies, but I'm afraid the goalposts and changing and now we are trying to avoid both copies and moves. The Rvalue function I present does seem to perform one move operation. However, if we cannot eliminate the move operation, I would suggest that the OP redesign their class so that moves are cheap, or just give up on the idea of this shorter syntax.

CodePudding user response:

There is a trick. It is NOT pretty but it works.

Make Reversed accept not a T, but a function returning T. Call it like that:

MyType value = Reversed(SomeFunction); // note no `SomeFunction()`

Here is a full implementation of Reversed:

template <class Generator>
MyType Reversed(Generator&& g)
{
  MyType t{g()};
  reverse(t);
  return t;
}

This produces no copies or moves. I checked.

If you feel particularly nasty, do this

#define Reversed(x) Reversed([](){return x;})

and go back to calling Reversed(SomeFunction()). Again, no copies or moves. Bonus points if you manage to squeeze it through a corporate code review.

  •  Tags:  
  • c
  • Related