防止大型结构之间不必要的复制

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

简而言之:编译器这样安排 datagetData 函数中,当它被 main() 调用时,实际上是 data 的别名在主要。

复制危险 2:datadata2

第二次“复制恐慌”是 setA()setB()。在这里你必须更多 pro-active,因为你在同一个函数中有两个有效的结构 - datagetData() 中的 data2。实际上,如果 DataDataFrom 只是大型结构 - 那么您将按照编写代码的方式进行大量从 data2data 的复制。

移动语义来拯救

但是,如果您的 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 在从函数 returned 时保证复制省略,这确保在某些情况下不会发生复制。您可以通过更改代码来确保这一点,使 Data 具有 = deleted 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