从函数返回参数时是否可以避免复制



假设我有带有一些就地操作的值类型。例如,像这样:

using MyType = std::array<100, int>;
void Reverse(MyType &value) {
std::reverse(value.begin(), value.end());
}

(类型和操作可能更复杂,但重点是操作就地工作,并且类型是普通可复制和可破坏的。请注意,MyType 足够大,可以考虑避免副本,但又足够小,以至于在堆上分配可能没有意义,并且由于它只包含原语,因此它不会从移动语义中受益。

我通常发现定义一个帮助程序函数很有帮助,该函数不会就地更改值,但会返回应用操作的副本。除此之外,还可以实现如下代码:

MyType value = Reversed(SomeFunction());

考虑到Reverse()就地运行,逻辑上应该可以在不复制SomeFunction()结果的情况下计算value。如何实现Reversed()以避免不必要的复制?我愿意将 Reversed() 定义为标头中的内联函数,如果这是启用此优化所必需的。

我可以想到两种实现它的方法:

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

这有利于返回值优化,但只有在参数value被复制到result之后

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

这可能需要调用者复制参数,除非它已经是一个右值,但我认为返回值优化不是以这种方式启用的(或者它是吗?),所以返回时有一个副本。

有没有一种方法来实现避免复制的Reversed(),理想情况下是以最近的C++标准保证的方式?

如果您确实想要就地反转字符串,以便对作为参数发送的字符串的更改在调用站点上可见,并且您还希望按值返回它,则除了复制它之外别无选择。它们是两个独立的实例。


一种替代方法:通过引用返回输入值。然后,它将引用您发送到函数的同一对象:

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
}

另一种选择:创建就地返回的对象。然后,您将返回一个单独的实例,其中 RVO 有效。没有移动或复制。不过,它将是一个与您发送到函数的实例不同的实例。

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

如果std::array可以像大多数其他容器一样从迭代器构造,第二种选择将起作用,但它们不能。一种解决方案可能是创建一个帮助程序来确保 RVO 正常工作:

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>{});
}

你的最后一个选择是要走的路(除了拼写错误):

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

[N]RVO不适用于return result;,但至少它是隐式移动的,而不是复制的。

您将有一个副本 + 一个移动,或两次移动,具体取决于参数的值类别。

有一个技巧。它不漂亮,但它有效。

Reversed接受的不是 T,而是返回 T 的函数。

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

以下是 Reversed 的完整实现:

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

这不会产生副本或移动。我检查了。

如果你觉得特别讨厌,就这样做

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

然后回去打电话给Reversed(SomeFunction()).同样,没有复制或移动。如果您设法通过公司代码审查来挤压它,则可以获得奖励积分。

可以使用帮助程序方法将就地操作转换为可用于 Rvalues 的操作。 当我在 GCC 中对此进行测试时,它会导致一次移动操作,但没有副本。 模式如下所示:

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

以下是我用来测试此模式是否会导致副本的完整代码:

#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 %pn", this);
}
MyType(const MyType & other) {
contents = new int[42];
memcpy(contents, other.contents, sizeof(int) * 42);
printf("Copied from %p to %pn", &other, this);
}
MyType(MyType && other) {
contents = other.contents;
other.contents = nullptr;
printf("Moved from %p to %pn", &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 modificationn");
MyType x = SomeFunction();
Reversed(x);
printf("%dn", x.contents[41]);
printf("RValue modificationn");
MyType y = Reversed(SomeFunction());
printf("%dn", y.contents[41]);
}

我不确定标准是否保证了这种副本的缺失,但我会这么认为,因为有些对象是不可复制的。

注意:最初的问题只是关于如何避免复制,但恐怕目标正在发生变化,现在我们正试图避免复制和移动。 我介绍的 Rvalue 函数似乎确实执行一次移动操作。 但是,如果我们不能消除移动操作,我建议 OP 重新设计他们的类,以便移动更便宜,或者干脆放弃这种较短语法的想法。

当你写的时候

MyType value = Reversed(SomeFunction());

我看到发生了两件事:Reversed将执行 RVO,因此它直接写入valueSomeFunction要么被复制到Reversed的参数中,要么创建一个临时对象并传递引用。无论你怎么写,都会至少有 2 个对象,你必须从一个反转到另一个。

编译器无法执行我所说的 AVO,即参数值优化。您希望将Reversed函数的参数存储在函数的返回值中,以便可以执行就地操作。有了这个功能,编译器可以执行 RVO-AVO-RVO,SomeFunction直接在最终的value变量中创建它的返回值。

但我认为你可以这样做:

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

换一种方式看:假设您确实找到了一种方法,让Reveresed执行就地操作,然后在

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

SomeFunction将创建一个临时的,但编译器必须将该临时的生存期延长到value的生存期。这在直接赋值中有效,但编译器如何知道Reversed只会传递临时通道?

从答案和评论来看,共识似乎是没有办法在C++实现这一目标。

这是没有可用MyType Reversed(MyType)实现的一般答案是有道理的,因为编译器不知道返回值与参数相同,因此它必须为它们分配单独的空间。

但看起来即使有 Reversed() 的实现可用,GCC 和 Clang 都不会优化副本:https://godbolt.org/z/KW6Y3vsdf

所以我认为短篇故事是,我所要求的是不可能的。如果避免复制很重要,调用方应明确写入:

MyType value = SomeFunction();
Reverse(value);
// etc.

相关内容

  • 没有找到相关文章