4

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?

Maks Verver
  • 661
  • 4
  • 14
  • 2
    The last option implicitly moves `result` when returning. It's the way to go. – HolyBlackCat Jun 20 '22 at 20:10
  • 1
    pretty sure `MyType Reverse(MyType &value) { return {value.rbegin(), value.rend()}; }` is all you need. – NathanOliver Jun 20 '22 at 20:11
  • _"t should be logically possible to calculate value without copying the result from SomeFunction()"_ - How? If you do the change in-place to one variable and want to return a second variable, it'll be a copy. You could return a reference to the input-variable though. Unrelated: `std::array<26, int>` should be `std::array` – Ted Lyngmo Jun 20 '22 at 20:19
  • _"`Reversed2(MyType value) { ...; return value; }` - I don't think return-value optimization is enabled this way (or is it?)"_ - No, it's not. It's even prohibited. – Ted Lyngmo Jun 20 '22 at 20:21
  • @NathanOliver: that might work great for the example but I was interested in the general case, where MyType may be a random struct with a bunch of stuff in it, and Reverse() could be any in-place operation. – Maks Verver Jun 20 '22 at 20:33
  • @TedLyngmo: Thanks for confirming RVO wouldn't work for Reversed2(). For your previous comment: I can manually rewrite "MyType value = Reversed(SomeFunction())" as "MyType value = SomeFunction(); Reverse(value);" which seems somewhat-obviously equivalent. I'm looking for a way to implemented "Reversed()" so that the compiler realizes that too and rewrites the former to the latter (which doesn't use a copy). – Maks Verver Jun 20 '22 at 20:36
  • @MaksVerver Well, the trickiest part with this is that we're working with an array (even though packaged in a `std::array`). Need to think about it but if you send in a temporary `std::array` to a function, like in `MyType value = Reversed(SomeFunction());` I can't see how you can skip the copy. – Ted Lyngmo Jun 20 '22 at 20:55
  • @MaksVerver That's impossible. It would require `SomeFunction` to construct the object directly into `value`. But _only_ if `Reversed` is a function of the kind you want. But nothing in the type of the function can indicate whether or not it has this special behavior you want. Function definitions are not required to be known at point of use, so the language can't apply the behavior you want. It would require some new special annotation to function declarations. Of course, the compiler is free to inline a function call and optimize under the as-if rule as always. – user17732522 Jun 20 '22 at 21:03

6 Answers6

5

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 std::move(value);       // moving doesn't really matter for ints
}

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>{});
}
Ted Lyngmo
  • 93,841
  • 5
  • 60
  • 108
  • 1
    The 1st alternative won't work for `Reversed(SomeFunction())` since an lvalue reference can't bind to an rvalue. Maybe use `MyType&&` instead, modify the temp, and then `move()` it into the final `value`. – Remy Lebeau Jun 20 '22 at 20:36
  • @RemyLebeau True ... and I think I have other problems here. The second version would be fine if `std::array` could be initialized by iterators. :-) Back to the drawing board. – Ted Lyngmo Jun 20 '22 at 20:42
  • @RemyLebeau Arrays are a bit tricky... but I came up with "something". Not sure if the `constexpr` array size hack is UB. I guess downvotes will tell :-) – Ted Lyngmo Jun 20 '22 at 21:19
  • 1
    `value.size() is somehow not constexpr, so a hack:`: That's specifically because you can never use non-constant-initialized reference variables in constant expressions, even if no lvalue-to-rvalue is applied. Hopefully that will be fixed (there is a proposal). Currently there is no nice solution that works for general range types. (See [my question](https://stackoverflow.com/questions/70482497/detecting-compile-time-constantness-of-range-size).) For `std::array` specifically you can use `std::tuple_size_v>`. You approach technically could fail due to padding. – user17732522 Jun 20 '22 at 21:39
  • @user17732522 Beautiful! Many thanks! The `remove_cvref` part: Would it be able to fail without it? – Ted Lyngmo Jun 20 '22 at 21:41
  • 1
    A sorry, I first wrote `decltype(value)` which requires it. If you use `MyType` directly it is not necessary. – user17732522 Jun 20 '22 at 21:46
2

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.

HolyBlackCat
  • 78,603
  • 9
  • 131
  • 207
  • (Sorry, I may have missed the typo?) Can you explain how moving helps if MyType is a large struct of primitives, e.g. MyType = std::array rather than std::vector with a size of 1000? In the vector case, I agree that moving is the way to go. In the array case, it seems like the move would be just as expensive as the copy, and I'm interested in figuring out a way to avoid the copy (if possible). – Maks Verver Jun 20 '22 at 20:31
  • @MaksVerver The typo is `return result;`, should be `return value;`. For a large `std::array` it doesn't help, yes. For those I wouldn't trust anything other than an in-place reverse. – HolyBlackCat Jun 20 '22 at 20:38
  • thanks, I fixed the typo in the question. – Maks Verver Jun 20 '22 at 20:47
  • I confirmed that this answer gives the same performance as mine (one move operation, no copy). Perhaps Rvalue reference arguments aren't needed. But if you want `Reversed2` and `Reverse` to have the same name and let the compiler figure out which one to use based on context, Rvalue reference arguments seem to be the way to go. – David Grayson Jun 20 '22 at 20:59
2

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 are 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.

David Grayson
  • 84,103
  • 24
  • 152
  • 189
  • Your code has undefined behavior. You pass `x` into `Reversed` and `Reversed` moves that into its discarded return value. That means `x` in main is in a moved from state. https://godbolt.org/z/M53aEaaba – NathanOliver Jun 20 '22 at 20:51
  • Oops, I forgot to use `delete[]` in the destructor and used `delete` by mistake. Thanks for pointing that out and showing me a new way to test my code. If you use `delete[]` all the errors go away and I don't believe this is undefined behavior. You just have to make sure that objects in the moved state can be cleanly destructed, and I did that. – David Grayson Jun 20 '22 at 20:53
2

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.

n. m. could be an AI
  • 112,515
  • 14
  • 128
  • 243
  • This is very clever and I can confirm it works as required, but it's probably a bit *too* clever for me to use in actual code, especially since the macro would be needed to support the general case where SomeFunction() can also take some arguments. – Maks Verver Jun 21 '22 at 14:25
  • 1
    Parameters are not a problem, you my need to capture everything by reference in the lambda for that: `#define Reversed(x) Reversed([&](){return (x);})` But if you think it's too clever to be used in production code, you are absolutely right. – n. m. could be an AI Jun 21 '22 at 14:27
0

When you write

MyType value = Reversed(SomeFunction());

I see 2 things happen: Reversed will do RVO so it directly writes to value and SomeFunction either gets coppied into the argument for Reversed or creates a temporary object and you pass a reference. No matter how you write it there will be at least 2 objects and you have to reverse from one to the other.

There is no way for the compiler to do what I would call an AVO, argument value optimization. You want the argument to the Reversed function to be stored in the return value of the function so you can do in-place operations. With that feature the compiler could do RVO-AVO-RVO and SomeFunction would create it's return value directly in the final value variable.

But you could do this I think:

MyType &&value = SomeFunctio();
reverse(value);

Looking at it another way: Say you do figure out a way for Reveresed to do in-place operations then in

MyType &&value = Reversed(SomeFunction());

the SomeFunction would create a temporary but then the compiler has to extend the lifetime of that temporary to the lifetime of value. This works in direct assignment but how is the compiler supposed to know that Reversed will just pass the temporary through?

Goswin von Brederlow
  • 11,875
  • 2
  • 24
  • 42
0

From the answers and comments it looks like the consensus is that there is no way to achieve this in C++.

It makes sense that this is the general answer without the implementation of MyType Reversed(MyType) available, since the compiler would have no clue that the return value is the same as the argument, so it would necessarily allocate separate spaces for them.

But it looks like even with the implementation of Reversed() available, neither GCC nor Clang will optimize away the copy: https://godbolt.org/z/KW6Y3vsdf

So I think the short story is that what I was asking for isn't possible. If it's important to avoid the copy, the caller should explicitly write:

MyType value = SomeFunction();
Reverse(value);
// etc.
Maks Verver
  • 661
  • 4
  • 14