Подтвердить что ты не робот

Могу ли я использовать memcpy в С++ для копирования классов, у которых нет указателей или виртуальных функций

Скажем, у меня есть класс, что-то вроде следующего:

class MyClass
{
public:
  MyClass();
  int a,b,c;
  double x,y,z;
};

#define  PageSize 1000000

MyClass Array1[PageSize],Array2[PageSize];

Если мой класс не имеет указателей или виртуальных методов, безопасно ли использовать следующее?

memcpy(Array1,Array2,PageSize*sizeof(MyClass));

Причина, по которой я спрашиваю, заключается в том, что я имею дело с очень большими коллекциями выгружаемых данных, как описано здесь, где производительность имеет решающее значение, а memcpy предлагает значительную производительность преимущества перед итерационным назначением. Я подозреваю, что это должно быть хорошо, поскольку указатель 'this' является неявным параметром, а не чем-либо сохраненным, но есть ли другие скрытые гадости, о которых я должен знать?

Изменить:

В соответствии с комментариями sharptooths данные не содержат никаких дескрипторов или аналогичной справочной информации.

Как и в комментарии Paul R, я профилировал код, и в этом случае избежать создания конструктора копии в 4,5 раза. Часть причины здесь заключается в том, что мой шаблонный класс массива несколько более сложный, чем приведенный упрощенный пример, и называет место "новым" при распределении памяти для типов, которые не позволяют мелкого копирования. Это фактически означает, что вызывается конструктор по умолчанию, а также конструктор копирования.

Второе редактирование

Возможно, стоит отметить, что я полностью согласен с тем, что использование memcpy таким образом является плохой практикой и его следует избегать в общих случаях. Конкретный случай, в котором он используется, является частью высокопроизводительного шаблонного массива класса, который включает в себя параметр AllowShallowCopying, который будет вызывать memcpy, а не конструктор копирования. Это имеет большие последствия для операций, таких как удаление элемента вблизи начала массива и подкачки данных в и из вторичного хранилища. Лучшим теоретическим решением было бы преобразование класса в простую структуру, но при условии, что это связано с большим количеством рефакторинга большой базы кода, избегая этого, я не хочу этого делать.

4b9b3361

Ответ 1

Позвольте мне дать вам эмпирический ответ: в нашем приложении в реальном времени мы делаем это все время, и все работает отлично. Это имеет место в MSVC для Wintel и PowerPC и GCC для Linux и Mac даже для классов с конструкторами.

Я не могу приводить главу и стих стандарта С++ для этого, просто экспериментальные данные.

Ответ 2

В соответствии со стандартом, если конструктор экземпляра не предоставил программист для класса, компилятор будет синтезировать конструктор, который демонстрирует инициализацию по умолчанию по умолчанию. (12.8.8) Однако в 12.8.1 в Стандарте также говорится:

Объект класса может быть скопирован в два способами, посредством инициализации (12.1, 8.5), в том числе для аргумента функции (5.2.2) и для значения функции return (6.6.3) и присваиванием (5.17). Концептуально эти два операции выполняются копией конструктор (12.1) и назначение копии оператор (13.5.3).

Оперативное слово здесь "концептуально", которое, согласно Lippman дает разработчикам компилятора "выход" для фактической инициализации по порядку в "тривиальные" (12.8.6) неявно определенные конструкторы копирования.

На практике тогда компиляторы должны синтезировать конструкторы копирования для этих классов, которые проявляют поведение, как если бы они выполняли инициализацию по порядку. Но если в классе появляется "Побитовая копия семантики" (Lippman, стр. 43), то компилятору не нужно синтезировать конструктор копирования (что приведет к вызову функции, возможно, вложенному) и вместо этого побитовая копия. Это утверждение, по-видимому, поддерживается в ARM, но я еще не просмотрел его.

Использование компилятора для проверки того, что что-то стандартно совместимо, всегда является плохой идеей, но компиляция вашего кода и просмотр полученной сборки, похоже, подтверждают, что компилятор не выполняет инициализацию по порядку в синтезированном конструкторе копии, но делает memcpy вместо:

#include <cstdlib>

class MyClass
{
public:
    MyClass(){};
  int a,b,c;
  double x,y,z;
};

int main()
{
    MyClass c;
    MyClass d = c;

    return 0;
}

Сборка, сгенерированная для MyClass d = c;:

000000013F441048  lea         rdi,[d] 
000000013F44104D  lea         rsi,[c] 
000000013F441052  mov         ecx,28h 
000000013F441057  rep movs    byte ptr [rdi],byte ptr [rsi] 

... где 28h - sizeof(MyClass).

Это было скомпилировано в MSVC9 в режиме отладки.

EDIT:

Длинный и короткий этот пост таков:

1) До тех пор, пока выполнение побитовой копии будет иметь те же побочные эффекты, что и в случае с константной копией, Стандарт позволяет тривиальным неявным конструкторам копировать memcpy вместо экземпляров с константами.

2) Некоторые компиляторы фактически выполняют memcpy вместо того, чтобы синтезировать тривиальный конструктор копии, который делает по порядку копии.

Ответ 3

Вы могли бы. Но сначала спросите себя:

Почему бы просто не использовать экземпляр-копию, предоставленный вашим компилятором, чтобы сделать копию по-членски?

У вас есть определенные проблемы с производительностью, для которых вам нужно оптимизировать?

Текущая реализация содержит все типы POD: что происходит, когда кто-то ее меняет?

Ответ 4

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

Ответ 5

[...], но есть ли другие скрытые гадости Я должен знать?

Да: ваш код делает определенные предположения, которые не предлагаются и не документированы (если только вы их не задокументируете). Это кошмар для обслуживания.

Кроме того, ваша реализация в основном взламывает (если это необходимо, это не плохо), и это может зависеть (не обязательно) от того, как ваш текущий компилятор реализует вещи.

Это означает, что если вы обновите компилятор/инструментальную цепочку на один год (или пять) с этого момента (или просто измените настройки оптимизации в своем текущем компиляторе), никто не запомнит этот взлом (если вы не сделаете большие усилия, чтобы сохранить его видимым), и вы может закончиться с undefined поведением на ваших руках, а разработчики проклинают "кто бы это ни сделал" через несколько лет.

Это не то, что решение является необоснованным, это то, что оно (или будет) неожиданным для сопровождающих.

Чтобы свести к минимуму это (неожиданность?), я переместил бы класс в структуру в пространстве имен, основанное на текущем имени класса, без каких-либо внутренних функций в структуре. Затем вы даете понять, что вы смотрите на блок памяти и рассматриваете его как блок памяти.

Вместо:

class MyClass
{
public:
    MyClass();
    int a,b,c;
    double x,y,z;
};

#define  PageSize 1000000

MyClass Array1[PageSize],Array2[PageSize];

memcpy(Array1,Array2,PageSize*sizeof(MyClass));

Вы должны иметь:

namespace MyClass // obviously not a class, 
                  // name should be changed to something meaningfull
{
    struct Data
    {
        int a,b,c;
        double x,y,z;
    };

    static const size_t PageSize = 1000000; // use static const instead of #define


    void Copy(Data* a1, Data* a2, const size_t count)
    {
        memcpy( a1, a2, count * sizeof(Data) );
    }

    // any other operations that you'd have declared within 
    // MyClass should be put here
}

MyClass::Data Array1[MyClass::PageSize],Array2[MyClass::PageSize];
MyClass::Copy( Array1, Array2, MyClass::PageSize );

Таким образом, вы:

  • ясно, что MyClass:: Data - это структура POD, а не класс (двоичные они будут одинаковыми или очень близкими - то же самое, если я правильно помню), но таким образом это также видно для программистов, читающих код.

  • централизовать использование memcpy (если вам нужно перейти на std:: copy или что-то еще) через два года вы сделаете это в одной точке.

  • сохранить использование memcpy рядом с реализацией структуры POD.

Ответ 6

Вы можете использовать memcpy для копирования массива типов POD. И будет хорошей идеей добавить static assert для boost::is_pod is true. Теперь вы не являетесь классом POD.

Арифметические типы, типы перечисления, типы указателей и указатели на типы членов - это POD.

Cv-квалифицированная версия типа POD сама по себе является POD-типом.

Массив POD сам по себе POD. Структура или объединение, все нестатические элементы данных которых являются POD, сам POD, если он имеет:

  • Нет конструкторов, объявленных пользователем.
  • Нет частных или защищенных нестатических данных.
  • Нет базовых классов.
  • Нет виртуальных функций.
  • Нет нестатических элементов данных ссылочного типа.
  • Нет пользовательского оператора присваивания копий.
  • Нет определяемого пользователем деструктора.

Ответ 7

Я заметил, что вы признаете, что здесь есть проблема. И вы знаете о потенциальных недостатках.

Мой вопрос - это вопрос обслуживания. Чувствуете ли вы уверенность, что никто никогда не будет включать поле в этом классе, которое бы стимулировало вашу большую оптимизацию? Я не знаю, я инженер, а не пророк.

Итак, вместо того, чтобы пытаться улучшить операцию копирования... почему бы не попытаться вообще избежать этого?

Можно ли изменить структуру данных, используемую для хранения, чтобы остановить перемещение элементов вокруг... или, по крайней мере, не так много.

Например, знаете ли вы о blist (модуль Python). B + Дерево может позволить доступ к индексу с характеристиками, весьма похожими на векторы (например, немного медленнее, правда), минимизируя количество элементов, которые нужно перемешать при вставке/удалении.

Вместо того, чтобы идти быстро и грязно, возможно, вам стоит сосредоточиться на поиске лучшей коллекции?

Ответ 8

Вызов memcpy для классов, отличных от POD, - это поведение undefined. Я предлагаю следовать совету Кирилла для утверждения. Использование memcpy может быть быстрее, но если операция копирования не является критически важной для вашего кода, просто используйте побитовое копирование.

Ответ 9

Говоря о случае, о котором вы говорите, я предлагаю вам объявить struct вместо class. Это делает его гораздо легче читать (и менее спорно:)) и спецификатор доступа по умолчанию является общедоступным.

Конечно, вы можете использовать memcpy в этом случае, но будьте осторожны, что добавление других типов элементов в структуре (например, классы С++) не рекомендуется (по очевидным причинам - вы не знаете, как memcpy будет влиять на них).

Ответ 10

Он будет работать, потому что класс (POD-) такой же, как структура (не полностью, доступ по умолчанию...), на С++. И вы можете скопировать структуру POD с memcpy.

Определение POD не было виртуальными функциями, конструктором, деконструктором нет виртуального наследования... и т.д.