Предотвращение ненужного копирования между большими структурами

У меня есть огромные структуры 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;
}

Поскольку данные огромны, в функции getData происходит копирование всей структуры данных. Можно ли это предотвратить каким-то элегантным способом?

У меня возникла идея:

static void getData( Data& data, const DataFrom& data2);

Но я бы предпочел получать данные как возвращаемое значение, а не как выходной параметр.


person wair92    schedule 14.08.2020    source источник
comment
Компилятор обычно оптимизирует это, поэтому нет необходимости копировать.   -  person Barmar    schedule 14.08.2020
comment
В вашем примере фактически не происходит никакого копирования, которое игнорируется NRVO.   -  person Igor Tandetnik    schedule 14.08.2020
comment
Вы можете = delete копировать построение и назначение и разрешать семантику перемещения только в том случае, если вы боитесь копий.   -  person François Andrieux    schedule 14.08.2020
comment
@FrançoisAndrieux: Это не обман.   -  person einpoklum    schedule 14.08.2020


Ответы (2)


Здесь следует рассмотреть две потенциальные опасности копирования:

Опасность копирования 1: конструкция снаружи getData()

В первой строке main(), где вы прокомментировали копию всей структуры данных - как отметили комментаторы, структура фактически не будет скопирована из-за оптимизации именованного возвращаемого значения или сокращенно NRVO. Вы можете прочитать об этом в этом хорошем сообщении в блоге, сделанном несколько лет назад:

Fluent{C++}: оптимизация возвращаемых значений

В двух словах: компилятор упорядочивает это так, что data внутри функции getData, когда она вызывается из main(), на самом деле является псевдонимом data в main.

Опасность копирования 2: data и data2

Вторая проблема с копированием связана с setA() и setB(). Здесь вы должны быть более активными, поскольку у вас есть две живые, действительные структуры в одной и той же функции — data и data2 внутри getData(). В самом деле, если Data и DataFrom просто большие структуры, то вам придется много копировать из data2 в data так, как вы написали свой код.

На помощь приходит семантика

Однако, если ваш DataFrom содержит ссылку на какое-то выделенное хранилище, скажем, std::vector<int> a вместо int[10000] a - вы можете переместить из своего DataFrom вместо копирования из него - имея getData() с подписью static Data getData(DataFrom&& data2). Подробнее о переезде читайте здесь:

Что такое семантика перемещения?

В моем примере это будет означать, что теперь вы будете использовать необработанный буфер data2.a для вашего data, не копируя содержимое этого буфера куда-либо еще. Но это будет означать, что вы больше не сможете использовать data2 после этого, так как его поле a было удалено, перемещено из.

... или просто поленитесь.

Вместо подхода, основанного на движении, вы можете попробовать что-то другое. Предположим, вы определили что-то вроде этого:

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 и не копируя ничего другого. При доступе может потребоваться разыменование указателя.


Другие примечания:

  • Ваш DataHandler определен как класс, но похоже, что он служит просто пространством имен. Вы никогда не создаете экземпляры обработчиков данных. Подумайте о том, чтобы прочитать:

    Почему и как использовать пространства имен в C++?

  • Мои предложения не связаны с C++17. Семантика перемещения была введена в C++11, и если вы выберете ленивый подход, он будет работать даже в C++98.

person einpoklum    schedule 14.08.2020

Поскольку вы пометили это тегом c ++17, вы можете написать свой код таким образом, чтобы предотвратить любое копирование (или его возможность), и если он скомпилируется, вы будете знать, что статически никакие копии не копируются. будет сделано.

C++17 гарантирует исключение копирования при возврате из функций, что гарантирует отсутствие копирования в определенных обстоятельствах. Вы можете убедиться в этом, изменив свой код так, чтобы Data имел копирующий конструктор = deleted, и изменив getData, чтобы он возвращал сконструированный объект. Если код вообще компилируется правильно, вы будете уверены, что копирование не произошло (поскольку копирование вызовет ошибку компиляции)

#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;
}

Это будет скомпилировано без каких-либо дополнительных копий — вы можете увидеть это здесь в проводнике компилятора

person Human-Compiler    schedule 14.08.2020
comment
Вероятно, вы захотите явно разрешить семантику перемещения. Прямо сейчас Data нельзя перемещать, что сильно ограничивает возможности с ним делать. - person François Andrieux; 14.08.2020
comment
1. Вам не нужно удалять копировальный центр, чтобы запустить NRVO, IANM. 2. Вы все еще делаете копии a и b, которые, по мнению OP, означают что-то огромное. 2. Я не уверен, что одобряю нарушение правила 0 того, что по сути является просто структурой. - person einpoklum; 14.08.2020
comment
@einpoklum Я не предлагаю вам вообще удалять копировальный центр; Я просто предположил, что это можно использовать как средство статического обнаружения, если когда-либо происходит копирование. Возможно, я мог бы сформулировать это лучше. Что касается второго пункта, я истолковал озабоченность ОП как наличие ненужных копий, поскольку явно требуется хотя бы одна копия. Что касается вашего третьего пункта, я согласен - я просто быстро увеличил пример OP; это не должно было быть высококачественным кодом. - person Human-Compiler; 14.08.2020
comment
@FrançoisAndrieux Это тоже хороший момент, и я согласен. Как я только что упомянул выше, я не пытался сделать этот высококачественный код, а скорее просто увеличил ввод OP в качестве демонстрации того, как это можно сделать. - person Human-Compiler; 14.08.2020