提问人:Maks Verver 提问时间:6/21/2022 最后编辑:Maks Verver 更新时间:6/21/2022 访问量:519
从函数返回参数时是否可以避免复制?
Is it possible to avoid a copy when returning an argument from a function?
问:
假设我有带有一些就地操作的值类型。例如,像这样的东西:
using MyType = std::array<100, int>;
void Reverse(MyType &value) {
std::reverse(value.begin(), value.end());
}
(类型和操作可能更复杂,但关键是操作就地工作,并且类型是可简单复制和可简单破坏的。请注意,MyType 足够大,可以考虑避免复制,但又足够小,以至于在堆上分配可能没有意义,并且由于它只包含基元,因此它不会从移动语义中受益。
我通常发现定义一个帮助程序函数很有帮助,该函数不会就地更改值,但会返回一个应用了操作的副本。除其他事项外,这支持如下代码:
MyType value = Reversed(SomeFunction());
考虑到就地操作,从逻辑上讲,应该在不复制结果的情况下进行计算。如何实现以避免不必要的副本?我愿意将 Reversed() 定义为标头中的内联函数,如果这是启用此优化所必需的。Reverse()
value
SomeFunction()
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;
}
这可能需要调用方复制参数,除非它已经是右值,但我认为返回值优化不是以这种方式启用的(或者是吗?),因此返回时会有一个副本。
有没有一种方法可以避免复制,最好是以最新的 C++ 标准保证的方式实现?Reversed()
答:
您的最后一个选择是要走的路(拼写错误除外):
MyType Reversed2(MyType value)
{
Reverse(value);
return value;
}
[N]RVO 不适用于 ,但至少它是隐式移动的,而不是复制的。return result;
您将有一个副本 + 一个移动,或两个移动,具体取决于参数的值类别。
评论
return result;
return value;
std::array
Reversed2
Reverse
如果确实要就地反转字符串,以便对作为参数发送的字符串的更改在调用站点上可见,并且还希望按值返回它,则别无选择,只能复制它。它们是两个独立的实例。
一种替代方法:通过引用返回输入值。然后,它将引用您发送到函数的同一对象:
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)};
}
如果可以像大多数其他容器一样从迭代器构造,则第二种选择将起作用,但它们不能。一种解决方案可能是创建一个帮助程序来确保 RVO 正常工作:std::array
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>{});
}
评论
Reversed(SomeFunction())
MyType&&
move()
value
std::array
constexpr
value.size() is somehow not constexpr, so a hack:
:这主要是因为您永远不能在常量表达式中使用非常量初始化的引用变量,即使没有应用左值到右值。希望这将得到解决(有一个提案)。目前,没有适用于一般范围类型的好解决方案。(见我的问题。具体来说,您可以使用 .从技术上讲,您的方法可能会因填充而失败。std::array
std::tuple_size_v<std::remove_cvref_t<MyType>>
decltype(value)
MyType
可以使用帮助程序方法将就地操作转换为可用于 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 %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]);
}
我不确定标准是否保证了这种副本的缺乏,但我认为是这样,因为有些对象是不可复制的。
注意:最初的问题只是关于如何避免复制,但恐怕球门柱正在发生变化,现在我们正试图避免复制和移动。我介绍的右值函数似乎确实执行了一次移动操作。但是,如果我们不能消除移动操作,我建议 OP 重新设计他们的类,以便移动更便宜,或者放弃这种更短语法的想法。
评论
x
Reversed
Reversed
x
delete[]
delete
delete[]
有一个诀窍。它并不漂亮,但它有效。
Make accept not a T, but a function return T.这样调用:Reversed
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())
评论
#define Reversed(x) Reversed([&](){return (x);})
当你写
MyType value = Reversed(SomeFunction());
我看到发生了两件事:将执行 RVO,因此它直接写入并复制到参数中,或者创建一个临时对象并传递引用。无论你怎么写,都会至少有 2 个对象,你必须从一个对象反转到另一个对象。Reversed
value
SomeFunction
Reversed
编译器无法执行我所说的 AVO,即参数值优化。您希望将函数的参数存储在函数的返回值中,以便可以执行就地操作。有了这个特性,编译器可以执行 RVO-AVO-RVO,并直接在最终变量中创建它的返回值。Reversed
SomeFunction
value
但我认为你可以这样做:
MyType &&value = SomeFunctio();
reverse(value);
换个角度看:假设你确实找到了一种方法来执行就地操作,然后在Reveresed
MyType &&value = Reversed(SomeFunction());
将创建一个临时的,但编译器必须将该临时的生存期延长到 的生存期。这在直接赋值中有效,但是编译器应该如何知道这只会传递临时通过?SomeFunction
value
Reversed
从答案和评论来看,共识是没有办法在 C++ 中实现这一点。
这是没有实现 available 的一般答案是有道理的,因为编译器不知道返回值与参数相同,因此它必然会为它们分配单独的空格。MyType Reversed(MyType)
但看起来即使有 Reversed() 的实现可用,GCC 和 Clang 都不会优化副本:https://godbolt.org/z/KW6Y3vsdf
所以我认为短篇小说是我所要求的是不可能的。如果避免复制很重要,调用方应显式写入:
MyType value = SomeFunction();
Reverse(value);
// etc.
评论
result
MyType Reverse(MyType &value) { return {value.rbegin(), value.rend()}; }
std::array<26, int>
std::array<int, 26>
Reversed2(MyType 值) { ...;返回值;}
- 我不认为返回值优化是以这种方式启用的(或者是吗?- 不,不是。它甚至被禁止。