Почему поведение std:: memcpy должно быть undefined для объектов, которые не являются TriviallyCopyable?

Из http://en.cppreference.com/w/cpp/string/byte/memcpy:

Если объекты не TriviallyCopyable (например, скаляры, массивы, C-совместимые структуры), поведение undefined.

В моей работе мы долгое время использовали std::memcpy для побитового обмена объектами, которые не являются TriviallyCopyable, используя:

void swapMemory(Entity* ePtr1, Entity* ePtr2)
{
   static const int size = sizeof(Entity); 
   char swapBuffer[size];

   memcpy(swapBuffer, ePtr1, size);
   memcpy(ePtr1, ePtr2, size);
   memcpy(ePtr2, swapBuffer, size);
}

и никогда не возникало проблем.

Я понимаю, что тривиально злоупотреблять std::memcpy не-TriviallyCopyable объектами и вызывать поведение undefined по течению. Однако мой вопрос:

Почему поведение std::memcpy было undefined при использовании с объектами, не связанными с TriviallyCopyable? Почему стандарт считает необходимым указать это?

UPDATE

Содержимое http://en.cppreference.com/w/cpp/string/byte/memcpy было изменено в ответ на этот пост и ответы на сообщение. В текущем описании говорится:

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

PS

Комментарий от @Cubbi:

@RSahu, если что-то гарантирует UB downstream, он отображает всю программу undefined. Но я согласен с тем, что в этом случае возможно обойти UB вокруг UB и соответствующим образом изменить cppreference.

Ответ 1

Почему поведение std::memcpy было undefined при использовании с объектами, не связанными с TriviallyCopyable?

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

Использование целевого объекта - вызов его функций-членов, обращение к его элементам данных - явно undefined [basic.life]/6 а также последующий неявный вызов деструктора [basic.life]/4 для целевых объектов, имеющих автоматическую продолжительность хранения. Обратите внимание, что поведение undefined ретроспективно. [intro.execution]/5:

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

Если в реализации указано, как объект мертв и обязательно подвержен дальнейшим операциям, которые undefined,... он может реагировать, изменяя семантику программ. От вызова memcpy. И это соображение становится очень практичным, когда мы думаем об оптимизаторах и некоторых допущениях, которые они делают.

Следует отметить, что стандартные библиотеки могут и могут оптимизировать определенные стандартные библиотечные алгоритмы для тривиально скопируемых типов. std::copy по указателям на тривиально-скопируемые типы обычно вызывает memcpy в базовых байтах. Таким образом, swap.
Поэтому просто придерживайтесь обычных генеративных алгоритмов и дайте компилятору какие-либо соответствующие низкоуровневые оптимизации - отчасти это было придумано для идеи тривиально-копируемого типа: определение законности определенных оптимизаций. Кроме того, это позволяет избежать вреда вашему мозгу, если вы будете беспокоиться о противоречивых и недоказанных частях языка.

Ответ 2

Оказывается, что стандарт ничего не говорит о поведении std::memcpy для объектов, которые не являются TriviallyCopyable. Содержимое http://en.cppreference.com/w/cpp/string/byte/memcpy было изменено в ответ на исходное сообщение и ответы на сообщение. В текущем описании говорится:

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

Ответ 3

Потому что стандарт говорит так.

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

Компилятор даже может принять ваш вызов memcpy и не делать ничего или форматировать жесткий диск. Зачем? Потому что стандарт говорит так. И делать ничего, безусловно, быстрее, чем перемещение бит вокруг, так почему бы не оптимизировать ваш memcpy для столь же эффективной более быстрой программы?

Теперь на практике существует много проблем, которые могут возникать, когда вы просто размахиваете битами в типах, которые этого не ожидают. Таблицы виртуальных функций могут быть настроены неправильно. Инструментарий, используемый для обнаружения утечек, не может быть настроен правильно. Объекты, личность которых включает их местоположение, полностью перепутаны вашим кодом.

Самое смешное, что using std::swap; swap(*ePtr1, *ePtr2); должен быть скомпилирован до memcpy для тривиально копируемых типов компилятором, а для других типов - поведение. Если компилятор может доказать, что копия копируется только битами, ее можно изменить на memcpy. И если вы можете написать более оптимальный swap, вы можете сделать это в пространстве имен объекта, о котором идет речь.

Ответ 4

Достаточно легко построить класс, в котором разрыв memcpy swap:

struct X {
    int x;
    int* px; // invariant: always points to x
    X() : x(), px(&x) {}
    X(X const& b) : x(b.x), px(&x) {}
    X& operator=(X const& b) { x = b.x; return *this; }
};

memcpy такой объект нарушает этот инвариант.

Это похоже на то, как реализованы стандартные потоки файлов и строк. Потоки в конечном итоге получаются из std::basic_ios, который содержит указатель на std::basic_streambuf. Потоки также содержат конкретный буфер в качестве члена (или под-объект базового класса), к которому указывает этот указатель в std::basic_ios.

Ответ 5

С++ не гарантирует для всех типов, что их объекты занимают смежные байты хранения [intro.object]/5

Объектом тривиально-копируемого или стандартного макета (3.9) занимают непрерывные байты хранения.

И действительно, через виртуальные базовые классы вы можете создавать несмежные объекты в основных реализациях. Я попытался построить пример, когда подобъект базового класса объекта x находится перед начальным адресом x. Чтобы визуализировать это, рассмотрим следующий график/таблицу, где горизонтальная ось - адресное пространство, а вертикальная ось - уровень наследования (уровень 1 наследуется от уровня 0). Поля, отмеченные dm, заняты непосредственными членами данных класса.

L | 00 08 16
--+---------
1 |    dm
0 | dm

Это обычная макет памяти при использовании наследования. Однако местоположение подобъекта виртуального базового класса не является фиксированным, поскольку оно может быть перемещено дочерними классами, которые также наследуются от одного и того же базового класса. Это может привести к тому, что объект уровня 1 (базовый класс) сообщает, что он начинается с адреса 8 и имеет размер 16 байт. Если мы наивно добавим эти два числа, мы подумаем, что он занимает адресное пространство [8, 24], хотя он фактически занимает [0, 16].

Если мы можем создать такой объект уровня 1, мы не сможем использовать memcpy для его копирования: memcpy будет обращаться к памяти, которая не принадлежит этому объекту (адреса от 16 до 24). В моей демонстрации, пойман как переполнение стека-буфера с помощью дезинфицирующего средства по адресу clang++.

Как построить такой объект? Используя несколько виртуальных наследований, я придумал объект, который имеет следующий макет памяти (указатели виртуальных таблиц отмечены как vp). Он состоит из четырех слоев наследования:

L  00 08 16 24 32 40 48
3        dm         
2  vp dm
1              vp dm
0           dm

Проблема, описанная выше, возникнет для подобъекта базового класса уровня 1. Его начальный адрес равен 32, и он имеет 24 байта (vptr, собственные члены данных и элементы данных уровня 0).

Вот код для такого макета памяти под clang++ и g++ @coliru:

struct l0 {
    std::int64_t dummy;
};

struct l1 : virtual l0 {
    std::int64_t dummy;
};

struct l2 : virtual l0, virtual l1 {
    std::int64_t dummy;
};

struct l3 : l2, virtual l1 {
    std::int64_t dummy;
};

Мы можем создать переполнение стека-буфера следующим образом:

l3  o;
l1& so = o;

l1 t;
std::memcpy(&t, &so, sizeof(t));

Здесь представлена ​​полная демоверсия, которая также печатает некоторую информацию о макете памяти:

#include <cstdint>
#include <cstring>
#include <iomanip>
#include <iostream>

#define PRINT_LOCATION() \
    std::cout << std::setw(22) << __PRETTY_FUNCTION__                   \
      << " at offset " << std::setw(2)                                  \
        << (reinterpret_cast<char const*>(this) - addr)                 \
      << " ; data is at offset " << std::setw(2)                        \
        << (reinterpret_cast<char const*>(&dummy) - addr)               \
      << " ; naively to offset "                                        \
        << (reinterpret_cast<char const*>(this) - addr + sizeof(*this)) \
      << "\n"

struct l0 {
    std::int64_t dummy;

    void report(char const* addr) { PRINT_LOCATION(); }
};

struct l1 : virtual l0 {
    std::int64_t dummy;

    void report(char const* addr) { PRINT_LOCATION(); l0::report(addr); }
};

struct l2 : virtual l0, virtual l1 {
    std::int64_t dummy;

    void report(char const* addr) { PRINT_LOCATION(); l1::report(addr); }
};

struct l3 : l2, virtual l1 {
    std::int64_t dummy;

    void report(char const* addr) { PRINT_LOCATION(); l2::report(addr); }
};

void print_range(void const* b, std::size_t sz)
{
    std::cout << "[" << (void const*)b << ", "
              << (void*)(reinterpret_cast<char const*>(b) + sz) << ")";
}

void my_memcpy(void* dst, void const* src, std::size_t sz)
{
    std::cout << "copying from ";
    print_range(src, sz);
    std::cout << " to ";
    print_range(dst, sz);
    std::cout << "\n";
}

int main()
{
    l3 o{};
    o.report(reinterpret_cast<char const*>(&o));

    std::cout << "the complete object occupies ";
    print_range(&o, sizeof(o));
    std::cout << "\n";

    l1& so = o;
    l1 t;
    my_memcpy(&t, &so, sizeof(t));
}

Живая демонстрация

Пример вывода (сокращенно, чтобы избежать вертикальной прокрутки):

l3::report at offset  0 ; data is at offset 16 ; naively to offset 48
l2::report at offset  0 ; data is at offset  8 ; naively to offset 40
l1::report at offset 32 ; data is at offset 40 ; naively to offset 56
l0::report at offset 24 ; data is at offset 24 ; naively to offset 32
the complete object occupies [0x9f0, 0xa20)
copying from [0xa10, 0xa28) to [0xa20, 0xa38)

Обратите внимание на два подчеркнутых крайних смещения.

Ответ 6

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

Одна из причин того, почему вызов memcpy считается undefined, заключается в том, чтобы предоставить компилятору максимально возможное количество возможностей для оптимизации на основе целевой платформы. Поскольку сам вызов является UB, компилятору разрешено делать странные, зависящие от платформы вещи.

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

Если бы вы использовали memcpy для объектов TriviallyCopyable на этой платформе, может произойти сбой низкого уровня INVALID OPCODE в самом вызове memcpy.

Не самый убедительный аргумент, возможно, но дело в том, что стандарт не запрещает его, что возможно только через вызов memcpy UB.

Ответ 7

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

Однако после копирования этих байтов объект, который вы скопировали, больше не может быть действительным объектом. Простой случай - это строковая реализация, когда большие строки выделяют память, но маленькие строки просто используют часть строкового объекта для хранения символов и сохраняют указатель на это. Указатель, очевидно, укажет на другой объект, поэтому все будет не так. Другим примером, который я видел, был класс с данными, который использовался только в очень немногих случаях, поэтому данные хранились в базе данных с адресом объекта в качестве ключа.

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

Ответ 8

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

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

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

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

Ответ 9

То, что я могу понять здесь, - это то, что для некоторых практических приложений стандарт С++ может быть ограниченным или, скорее, недостаточно разрешенным.

Как показано в других ответах memcpy быстро сокращается для "сложных" типов, но IMHO, он действительно должен работать для стандартных типов макета, пока memcpy не сломает то, что определены операции копирования и деструктор типа Standard Layout. (Обратите внимание, что четному классу TC разрешено иметь нетривиальный конструктор.) Стандарт только явно вызывает типы TC wrt. это, однако.

Недавний проект цитаты (N3797):

3.9 Типы

...

2 Для любого объекта (кроме подобъекта базового класса) тривиально тип копирования T, независимо от того, имеет ли объект допустимое значение типа T, базовые байты (1.7), составляющие объект, могут быть скопированы в массив char или без знака char. Если содержимое массива charили unsigned char копируется обратно в объект, объект должен впоследствии сохраняют свое первоначальное значение. [Пример:

  #define N sizeof(T)
  char buf[N];        T obj; // obj initialized to its original value
  std::memcpy(buf, &obj, N); // between these two calls to std::memcpy,       
                             // obj might be modified         
  std::memcpy(&obj, buf, N); // at this point, each subobject of obj of scalar type
                             // holds its original value 

-end пример]

3 Для любого тривиально-скопируемого типа T, если два указателя на T указывают на различные T-объекты obj1 и obj2, где ни obj1, ни obj2 не являются subobject базового класса, если базовые байты (1.7), составляющие obj1, являются скопированный в obj2, obj2 впоследствии будет иметь то же значение, что и obj1. [Пример:

T* t1p;
T* t2p;       
     // provided that t2p points to an initialized object ...         
std::memcpy(t1p, t2p, sizeof(T));  
     // at this point, every subobject of trivially copyable type in *t1p contains        
     // the same value as the corresponding subobject in *t2p

-end пример]

В стандарте здесь описываются тривиально скопируемые типы, но как был отмечен by @dyp выше, есть также стандартные типы макетов, которые этого не делают, поскольку насколько я вижу, обязательно пересекаются с тривиально копируемыми типами.

В стандарте говорится:

1.8 Объектная модель С++

(...)

5 (...) Объект тривиально-скопируемого или стандартного макета (3.9) должен занимать непрерывные байты хранения.

Итак, я вижу здесь следующее:

  • В стандарте ничего не говорится о типах, которые не могут быть тривиально скопированы. memcpy. (как уже упоминалось здесь несколько раз)
  • Стандарт имеет отдельную концепцию для стандартных типов макетов, которые занимают непрерывное хранилище.
  • Стандартная не позволяет явно разрешать или запрещать использование memcpy для объектов стандартной макеты, которые не могут быть тривиально скопированы.

Таким образом, похоже, что UB явно не вызвал UB, но это, безусловно, также не является тем, что называется неопределенным поведением поэтому можно было сделать вывод о том, что @underscore_d сделал в комментарии к принятому ответу:

(...) Вы не можете просто сказать "хорошо, это не был явно вызван как UB, поэтому он определил поведение!", что, по-видимому, составляет этот поток. N3797 3.9 пункты 2 ~ 3 не определяют, что memcpy делает для нетривиально-скопируемых объекты, поэтому (...) [t] шляпа практически функционально эквивалентно UB в моих глазах, поскольку оба бесполезны для написания надежного, то есть портативного кода

Я бы сделал вывод, что это касается UB, насколько переносимость идет (о, эти оптимизаторы), но я думаю, что с некоторым хеджированием и знаниями о конкретной реализации можно с этим справиться. (Просто убедитесь, что это стоит проблемы.)


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

Ссылка: Могу ли я использовать memcpy для записи в несколько соседних подкомпонентов стандартного макета?