假设我有带有一些就地操作的值类型。例如,像这样:
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,因此它直接写入value
SomeFunction
要么被复制到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.