防止大型结构之间不必要的复制
Prevent unnecessary copying between large structs
我有巨大的结构 DataFrom 和 Data(它们在现实中有不同的成员)。数据是从 DataFrom 创建的。
struct DataFrom{
int a = 1;
int b = 2;
};
static DataFrom dataFrom;
struct Data{
int a;
int b;
};
class DataHandler{
public:
static Data getData(const DataFrom& data2){
Data data;
setA(data, data2);
setB(data, data2);
return data;
}
private:
static void setA(Data& dest, const DataFrom& source){
dest.a = source.a;
}
static void setB(Data& dest, const DataFrom& source){
dest.b = source.b;
}
};
int main(){
auto data = DataHandler2::getData(dataFrom); // copy of whole Data structure
// ...
return 0;
}
由于Data很大,在getData函数中,复制了整个Data结构。可以以某种优雅的方式防止这种情况吗?
我有一个想法:
static void getData( Data& data, const DataFrom& data2);
但我更愿意将数据检索为 return 值,而不是输出参数。
这里有两个潜在的“复制危害”需要解决:
复制危害1:getData()
外的构造
在 main()
的第一行,您评论了“复制整个数据结构”——正如评论者所指出的,由于 Named Return价值优化,简称NRVO。您可以在几年前的这个不错的博客 post 中读到它:
Fluent{C++}: Return value optimizations
简而言之:编译器这样安排 data
在 getData
函数中,当它被 main()
调用时,实际上是 data
的别名在主要。
复制危险 2:data
和 data2
第二次“复制恐慌”是 setA()
和 setB()
。在这里你必须更多 pro-active,因为你在同一个函数中有两个有效的结构 - data
和 getData()
中的 data2
。实际上,如果 Data
和 DataFrom
只是大型结构 - 那么您将按照编写代码的方式进行大量从 data2
到 data
的复制。
移动语义来拯救
但是,如果您的 DataFrom
持有对某些已分配存储空间的引用,例如 std::vector<int> a
而不是 int[10000] a
- 您可以 移动 从你的 DataFrom
而不是从它复制 - 通过 getData()
和签名 static Data getData(DataFrom&& data2)
。在此处阅读有关移动的更多信息:
What is move semantics?
在我的示例中,这意味着您现在可以将 data2.a
的原始缓冲区用于您的 data
- 无需将该缓冲区的内容复制到其他任何地方。但这意味着您之后不能再使用 data2
,因为它的 a
字段已被蚕食,从.
移走
...或者只是“懒惰”。
您可以尝试其他方法,而不是 move-based 方法。假设你定义了这样的东西:
class Data {
protected:
DataFrom& source_;
public:
int& a() { return source_.a; }
int& b() { return source_.b; }
public:
Data(DataFrom& source) : source_(source) { }
Data(Data& other) : source_(other.source) { }
// copy constructor?
// assignment operators?
};
现在Data
不是一个简单的结构;它更像是 DataFrom
的外观(也许还有其他一些字段和方法)。这有点不方便,但好处是您现在创建一个 Data
仅引用 DataFrom
而没有复制任何其他内容。在 access 上,您可能需要取消引用指针。
其他说明:
您的 DataHandler
被定义为 class,但看起来它只是一个命名空间。您永远不会实例化“数据处理程序”。考虑阅读:
Why and how should I use namespaces in C++?
我的建议不涉及任何C++17。移动语义是在 C++11 中引入的,如果您选择“惰性”方法 - 即使在 C++98 中也可以工作。
因为你已经用 c++17 标记了它,你可以用一种方式来编写你的代码来防止任何复制发生(或能够发生),如果它编译你就会知道静态地不会制作副本。
C++17 在从函数 returned 时保证复制省略,这确保在某些情况下不会发生复制。您可以通过更改代码来确保这一点,使 Data
具有 = delete
d copy-constructor,并将 getData
更改为 return 构造对象。如果代码完全正确编译,您将确定没有发生复制(因为复制会触发 compile-error)
#include <iostream>
struct DataFrom{
int a = 1;
int b = 2;
};
static DataFrom dataFrom;
struct Data{
Data() = default;
Data(const Data&) = delete; // No copy
int a;
int b;
};
class DataHandler{
public:
static Data getData(const DataFrom& data2){
// construct it during return
return Data{data2.a, data2.b};
}
private:
static void setA(Data& dest, const DataFrom& source){
dest.a = source.a;
}
static void setB(Data& dest, const DataFrom& source){
dest.b = source.b;
}
};
int main(){
auto data = DataHandler::getData(dataFrom); // copy of whole Data structure
return 0;
}
这将在没有任何额外副本的情况下编译 -- 你可以看到它 here on compiler explorer
我有巨大的结构 DataFrom 和 Data(它们在现实中有不同的成员)。数据是从 DataFrom 创建的。
struct DataFrom{
int a = 1;
int b = 2;
};
static DataFrom dataFrom;
struct Data{
int a;
int b;
};
class DataHandler{
public:
static Data getData(const DataFrom& data2){
Data data;
setA(data, data2);
setB(data, data2);
return data;
}
private:
static void setA(Data& dest, const DataFrom& source){
dest.a = source.a;
}
static void setB(Data& dest, const DataFrom& source){
dest.b = source.b;
}
};
int main(){
auto data = DataHandler2::getData(dataFrom); // copy of whole Data structure
// ...
return 0;
}
由于Data很大,在getData函数中,复制了整个Data结构。可以以某种优雅的方式防止这种情况吗?
我有一个想法:
static void getData( Data& data, const DataFrom& data2);
但我更愿意将数据检索为 return 值,而不是输出参数。
这里有两个潜在的“复制危害”需要解决:
复制危害1:getData()
外的构造
在 main()
的第一行,您评论了“复制整个数据结构”——正如评论者所指出的,由于 Named Return价值优化,简称NRVO。您可以在几年前的这个不错的博客 post 中读到它:
Fluent{C++}: Return value optimizations
简而言之:编译器这样安排 data
在 getData
函数中,当它被 main()
调用时,实际上是 data
的别名在主要。
复制危险 2:data
和 data2
第二次“复制恐慌”是 setA()
和 setB()
。在这里你必须更多 pro-active,因为你在同一个函数中有两个有效的结构 - data
和 getData()
中的 data2
。实际上,如果 Data
和 DataFrom
只是大型结构 - 那么您将按照编写代码的方式进行大量从 data2
到 data
的复制。
移动语义来拯救
但是,如果您的 DataFrom
持有对某些已分配存储空间的引用,例如 std::vector<int> a
而不是 int[10000] a
- 您可以 移动 从你的 DataFrom
而不是从它复制 - 通过 getData()
和签名 static Data getData(DataFrom&& data2)
。在此处阅读有关移动的更多信息:
What is move semantics?
在我的示例中,这意味着您现在可以将 data2.a
的原始缓冲区用于您的 data
- 无需将该缓冲区的内容复制到其他任何地方。但这意味着您之后不能再使用 data2
,因为它的 a
字段已被蚕食,从.
...或者只是“懒惰”。
您可以尝试其他方法,而不是 move-based 方法。假设你定义了这样的东西:
class Data {
protected:
DataFrom& source_;
public:
int& a() { return source_.a; }
int& b() { return source_.b; }
public:
Data(DataFrom& source) : source_(source) { }
Data(Data& other) : source_(other.source) { }
// copy constructor?
// assignment operators?
};
现在Data
不是一个简单的结构;它更像是 DataFrom
的外观(也许还有其他一些字段和方法)。这有点不方便,但好处是您现在创建一个 Data
仅引用 DataFrom
而没有复制任何其他内容。在 access 上,您可能需要取消引用指针。
其他说明:
您的
DataHandler
被定义为 class,但看起来它只是一个命名空间。您永远不会实例化“数据处理程序”。考虑阅读:Why and how should I use namespaces in C++?
我的建议不涉及任何C++17。移动语义是在 C++11 中引入的,如果您选择“惰性”方法 - 即使在 C++98 中也可以工作。
因为你已经用 c++17 标记了它,你可以用一种方式来编写你的代码来防止任何复制发生(或能够发生),如果它编译你就会知道静态地不会制作副本。
C++17 在从函数 returned 时保证复制省略,这确保在某些情况下不会发生复制。您可以通过更改代码来确保这一点,使 Data
具有 = delete
d copy-constructor,并将 getData
更改为 return 构造对象。如果代码完全正确编译,您将确定没有发生复制(因为复制会触发 compile-error)
#include <iostream>
struct DataFrom{
int a = 1;
int b = 2;
};
static DataFrom dataFrom;
struct Data{
Data() = default;
Data(const Data&) = delete; // No copy
int a;
int b;
};
class DataHandler{
public:
static Data getData(const DataFrom& data2){
// construct it during return
return Data{data2.a, data2.b};
}
private:
static void setA(Data& dest, const DataFrom& source){
dest.a = source.a;
}
static void setB(Data& dest, const DataFrom& source){
dest.b = source.b;
}
};
int main(){
auto data = DataHandler::getData(dataFrom); // copy of whole Data structure
return 0;
}
这将在没有任何额外副本的情况下编译 -- 你可以看到它 here on compiler explorer