Почему поведение std:: memcpy не определено для объектов, которые не являются TriviallyCopyable?
от http://en.cppreference.com/w/cpp/string/byte/memcpy:
если объекты не TriviallyCopyable (например, скаляры, массивы, c-совместимые структуры), поведение не определено.
на моей работе, мы использовали 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 объектов и приводят к неопределенному поведению вниз по течению. Однако мой вопрос:
почему поведение std::memcpy
сам быть определено при использовании с TriviallyCopyable объекты? Почему стандарт считает необходимым это уточнять?
обновление
содержание http://en.cppreference.com/w/cpp/string/byte/memcpy были изменены в ответ на этот пост и ответы на сообщение. Текущее описание гласит:
если объекты не TriviallyCopyable (например, скаляры, массивы, c-совместимая структуры), поведение не определено, если программа не зависит от воздействия деструктора объекта (который не
memcpy
) и время жизни целевого объекта (которое заканчивается, но не начинаетсяmemcpy
) запускается другими способами, такими как размещение-новое.
PS
комментарий @Cubbi:
@RSahu если что-то гарантирует UB вниз по течению, это делает всю программу неопределенной. Но я согласен с тем, что в этом случае представляется возможным обойти UB и соответственно изменить cppreference.
9 ответов
почему поведение
std::memcpy
сам быть определено при использовании с TriviallyCopyable объекты?
это не так! Однако, как только вы копируете базовые байты одного объекта нетривиально копируемого типа в другой объект этого типа,целевой объект не жив. Мы уничтожили его, повторно используя его хранилище, и не оживили его вызовом конструктора.
использование целевого объекта-вызов его функций-членов, доступ к его членам данных-явно не определен[basic.жизнь]/6, а также последующий неявный вызов деструктора[basic.жизнь]/4 для целевых объектов, имеющих автоматическая длительность хранения. Обратите внимание, как неопределенное поведение-это ретроспектива. [вступление.выполнение]/5:
однако, если любое такое выполнение содержит неопределенную операцию, это Международный стандарт не устанавливает никаких требований в отношении осуществления выполнение этой программы с этим вводом (даже в отношении операции, предшествующие первой неопределенной операции).
если реализация обнаруживает, как объект мертв и обязательно подлежит дальнейшим операциям, которые не определены... он может реагировать, изменяя семантику ваших программ. От memcpy
вызовы далее. И это соображение становится очень практичным, как только мы думаем об оптимизаторах и определенных предположениях, которые они делают.
следует отметить, что однако стандартные библиотеки могут и позволяют оптимизировать некоторые стандартные алгоритмы библиотек для тривиально копируемых типов. std::copy
по указателям на тривиально копируемые типы обычно вызывает memcpy
на базовых байт. Как и swap
.
Поэтому просто придерживайтесь обычных общих алгоритмов и позвольте компилятору делать любые соответствующие низкоуровневые оптимизации-это отчасти то, для чего была изобретена идея тривиально копируемого типа: определение законность некоторых оптимизаций. Кроме того, это позволяет избежать повреждения мозга, беспокоясь о противоречивых и недооцененных частях языка.
потому что стандарт говорит так.
компиляторы могут предположить, что нетривиальные типы копируются только через их конструкторы копирования/перемещения/операторы присваивания. Это может быть для целей оптимизации (если некоторые данные являются частными, это может отложить его установку до копирования / перемещения).
компилятор даже бесплатно взять свой memcpy
позвоните и получите его ничего, или отформатировать жесткий диск. Почему? Потому что так сказано в стандарте. И ничего не делать определенно быстрее, чем перемещение бит вокруг, так почему бы не оптимизировать ваш memcpy
для равнозначной более быстрой программы?
теперь, на практике, есть много проблем, которые могут возникнуть, когда вы просто Блит вокруг битов в типах, которые этого не ожидают. Таблицы виртуальных функций могут быть настроены неправильно. Приборы, используемые для обнаружения утечек, могут быть установлены неправильно. Объекты, чья идентичность включает их местоположение, полностью запутываются вашим кодом.
действительно забавно это using std::swap; swap(*ePtr1, *ePtr2);
должен быть составлен до memcpy
для тривиально копируемых типов компилятором и для других типов определяется поведение. Если компилятор может доказать, что copy-это просто скопированные биты, он может изменить его на memcpy
. И если вы можете написать более оптимальный swap
, вы можете сделать это в пространстве имен объекта в вопрос.
достаточно легко построить класс, где это memcpy
- based 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
ing такой объект нарушает этот инвариант.
GNU C++11 std::string
делает именно это с короткими строками.
это похоже на то, как реализованы стандартные потоки файлов и строк. Потоки в конечном итоге происходят от std::basic_ios
, который содержит указатель на std::basic_streambuf
. Потоки также содержат определенный буфер в качестве члена (или базового класса sub-object), на который этот указатель в std::basic_ios
указывает.
C++ не гарантирует для всех типов, что их объекты занимают непрерывные байты хранилища [вступление.объектом]/5
объект тривиально копируемого типа или типовой формы (3.9) должен занимают смежные байты хранения.
и действительно, через виртуальные базовые классы вы можете создавать несмежные объекты в основных реализациях. Я попытался построить пример, где субобъект базового класса объекта это до x
начальный адрес. Чтобы визуализировать это, рассмотрим следующий график / таблицу, где горизонтальная ось-адресное пространство, а вертикальная ось-уровень наследования (уровень 1 наследуется от уровня 0). Поля, отмеченные dm
занимают прямые члены данных класса.
L | 00 08 16 --+--------- 1 | dm 0 | dm
это обычный макет памяти при использовании наследования. Однако расположение виртуального базового класса подобъекта не фиксируется, так как это может быть перемещено дочерними классами, которые также наследуются от того же базового класса практически. Это может привести к ситуации, что объект уровня 1 (базовый класс sub)сообщает, что он начинается с адреса 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)
обратите внимание на два подчеркнутых смещения конца.
многие из этих ответов упоминают, что memcpy
может нарушать инварианты в классе, что вызовет неопределенное поведение позже (и что в большинстве случаев должно быть достаточной причиной, чтобы не рисковать этим), но это, похоже, не то, что вы действительно спрашиваете.
одна из причин, почему memcpy
сам вызов считается неопределенным поведением, чтобы дать как можно больше места компилятору для оптимизации на основе целевой платформы. Имея сам вызов быть UB, компилятор разрешено делать странные, зависящие от платформы вещи.
рассмотрим этот (очень надуманный и гипотетический) пример: для конкретной аппаратной платформы может быть несколько разных видов памяти, причем некоторые из них быстрее, чем другие для разных операций. Например, может быть своего рода специальная память, которая позволяет создавать дополнительные быстрые копии памяти. Поэтому компилятору для этой (мнимой) платформы разрешено размещать все TriviallyCopyable
типы в этом специальном память и реализация memcpy
использовать специальные аппаратные инструкции, которые работают только на эту память.
если вы используете memcpy
наTriviallyCopyable
объекты на этой платформе, может быть какой-то низкоуровневый недопустимый сбой кода операции на вызов.
не самый убедительный аргумент, возможно, но дело в том, что стандарт не запрещает это, что возможно только путем внесения memcpy
вызов УБ.
memcpy скопирует все байты или в вашем случае заменит все байты, просто отлично. Чрезмерно усердный компилятор может принять "неопределенное поведение" как оправдание для всех видов озорства, но большинство компиляторов этого не сделают. И все же это возможно.
однако после копирования этих байтов объект, в который вы их скопировали, может больше не быть допустимым объектом. Простой случай - это реализация строки, где большие строки выделяют память, но маленькие строки просто используют часть string объект для хранения символов, и сохранить указатель на это. Указатель, очевидно, будет указывать на другой объект, поэтому все будет неправильно. Другим примером, который я видел, был класс с данными, который использовался только в очень немногих экземплярах, так что данные хранились в базе данных с адресом объекта в качестве ключа.
теперь, если ваши экземпляры содержат мьютекс, например, я бы подумал, что перемещение этого может быть серьезной проблемой.
другая причина memcpy
является ли UB (помимо того, что было упомянуто в других ответах - это может нарушить инварианты позже), что для стандарта очень сложно сказать точно что будет.
для нетривиальных типов стандарт очень мало говорит о том, как объект выложен в памяти, в каком порядке размещаются члены, где находится указатель vtable, каким должно быть заполнение и т. д. Компилятор имеет огромное количество свободы в принятии решений этот.
в результате, даже если стандарт хотел разрешить memcpy
в этих "безопасных" ситуациях было бы невозможно указать, какие ситуации безопасны, а какие нет, или когда именно реальный UB будет запущен для небезопасных случаев.
Я полагаю, что вы можете утверждать, что эффекты должны быть определены или не определены, но я лично чувствую, что это будет слишком глубоко копаться в специфике платформы и давать немного слишком много легитимности к чему-то, что в общем случае довольно небезопасно.
во-первых, обратите внимание, что бесспорно, что вся память для изменяемых объектов C/C++ должна быть не типизированной, не Специализированной, пригодной для любого изменяемого объекта. (Я предполагаю, что память для глобальных переменных const гипотетически может быть набрана, просто нет смысла с таким гипер-усложнением для такого крошечного углового случая.) в отличие от Java, в C++ не имеет типизированного выделения динамического объекта: new Class(args)
в Java является типизированным созданием объекта: создание объекта четко определенного типа, который может жить в типизированной памяти. С другой стороны, выражение C++new Class(args)
- это просто тонкая оболочка для ввода текста вокруг выделения памяти без типа, эквивалентная new (operator new(sizeof(Class)) Class(args)
: объект создается в "нейтральной памяти". Изменение этого означало бы изменение очень большой части C++.
запрещение операции копирования битов (выполняется ли memcpy
или эквивалентная пользовательская байтовая копия) на некотором типе дает большую свободу реализации для полиморфных классов (с виртуальными функции), и другие так называемые "виртуальные классы" (не стандартный термин), то есть классы, которые используют virtual
ключевое слово.
реализация полиморфных классов может использовать глобальную ассоциативную карту адресов, которые связывают адрес полиморфного объекта и его виртуальные функции. Я считаю, что этот вариант серьезно рассматривался при разработке первых итераций языка C++ (или даже "C с классами"). Эта карта полиморфных объектов может использовать специальный CPU функции и специальная ассоциативная память (такие функции не доступны пользователю C++).
конечно, мы знаем, что все практические реализации виртуальных функций используют vtables (постоянную запись, описывающую все динамические аспекты класса) и помещают vptr (указатель vtable) в каждый полиморфный субобъект базового класса, поскольку этот подход чрезвычайно прост в реализации (по крайней мере, для простейших случаев) и очень эффективен. Нет глобального реестра полиморфных объектов ни в одном реальном реализации, за исключением, возможно, в режиме отладки (я не знаю такого режима отладки).
стандарт C++ сделал отсутствие глобального реестра несколько официальных сказать, что вы можете пропустить вызов деструктора при повторном использовании памяти объект, пока вы не зависите от "побочных эффектов" этого вызова деструктора. (Я считаю, что это означает, что" побочные эффекты " создаются пользователем, то есть телом деструктора, а не реализацией, созданной автоматически сделал деструктор реализацией.)
потому что на практике во всех реализациях компилятор просто использует скрытые члены vptr (указатель на vtables), и эти скрытые члены будут скопированы должным образомmemcpy
; как если бы вы сделали простую копию структуры C, представляющую полиморфный класс (со всеми его скрытыми членами). Битовые копии или полные копии элементов структуры C (полная структура C включает скрытые элементы) будут вести себя точно так же, как вызов конструктора (как сделано путем размещения new), поэтому все, что вам нужно сделать, пусть компилятор думает, что вы могли бы назвать размещение new. Если вы выполняете строго внешний вызов функции (вызов функции, которая не может быть встроена и реализация которой не может быть проверена компилятором, например вызов функции, определенной в динамически загружаемом блоке кода, или системный вызов), компилятор просто предположит, что такие конструкторы могли быть вызваны кодом, который он не может проверить. в поведение memcpy
здесь определяется не стандартом языка, а компилятором ABI (Application Binary Interface). поведение сильно внешнего вызова функции определяется ABI, а не только стандартом языка. Вызов потенциально inlinable функции определяется языком, поскольку его определение можно увидеть (либо во время компилятора, либо во время глобальной оптимизации времени связи).
поэтому на практике, учитывая соответствующие "заборы компилятора" (например, вызовите внешнюю функцию или просто asm("")
), вы можете memcpy
классы, которые используют только виртуальные функции.
конечно, вы должны быть разрешены семантическим языком, чтобы сделать такое размещение новым, когда вы делаете memcpy
: вы не можете волей-неволей переопределить динамический тип существующего объекта и притвориться, что вы не просто разрушили старый объект. Если у вас есть не const global, static, automatic, member subobject, array subobject, вы можете перезаписать его и поместить другой, несвязанный объект; но если динамический тип отличается, вы не можете притворяться, что это все тот же объект или подобъект:
struct A { virtual void f(); };
struct B : A { };
void test() {
A a;
if (sizeof(A) != sizeof(B)) return;
new (&a) B; // OK (assuming alignement is OK)
a.f(); // undefined
}
изменения полиморфного типа существующего объекта просто не допускается: новый объект не имеет никакого отношения к a
за исключением области памяти: непрерывные байты, начинающиеся с &a
. У них разные типы.
[стандарт сильно разделен на Ли *&a
смогите быть использовано (в типичной плоской памяти машины) или (A&)(char&)a
(в любом случае) для ссылки на новый объект. Составители компиляторов не разделены: вы не должны этого делать. Это глубокий дефект в C++, возможно, самый глубокий и тревожный.]
но вы не можете в переносном коде выполнять побитовое копирование классов, использующих виртуальное наследование, поскольку некоторые реализации реализуют эти классы с указателями на виртуальные базовые подобъекты: эти указатели, которые были правильно инициализированы конструктором самого производного объекта, имели бы их значение скопировано memcpy
(как простой член мудрой копии структуры C, представляющей класс со всеми его скрытыми членами) и не будет указывать подобъект производного объекта!
другие ABI используют смещения адресов для поиска этих базовых подобъектов; они зависят только от типа самого производного объекта, например final overriders и typeid
, и таким образом можно хранить в vtable. Об этих реализациях,memcpy
будет работать как гарантировано ABI (с вышеуказанным ограничением на изменение типа существующего объекта).
в любом случае это полностью проблема представления объекта, то есть проблема ABI.
что я могу понять здесь, так это то, что-для некоторых практических приложений-стандарт C++мая быть ограничительным или, вернее, недостаточно permittive.
как показано в другие ответы memcpy
быстро ломается для "сложных" типов, но, ИМХО, это на самом деле должны работа для стандартных типов компоновки до тех пор, пока memcpy
не нарушает то, что делают определенные операции копирования и деструктор стандартного типа макета. (Обратите внимание, что четный класс TC разрешено имеет нетривиальный конструктор.) Стандарт только явно вызывает типы TC wrt. это, однако.
недавний проект цитаты (N3797):
3.9 типы
...
2 для любого объекта (кроме базового класса подобъекта) из тривиально копируемый тип T, независимо от того, содержит ли объект допустимое значение типа T, базовые байты (1.7), составляющие объект, могут быть скопированы в - массив char или unsigned 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
-конец примера ]
3 для любого тривиально копируемого типа T, если два указателя на T указывают на различные t объекты obj1 и obj2, где ни obj1, ни obj2 не являются субобъект базового класса, если базовые байты (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
-конец примера ]
стандарт здесь говорит о тривиально копируемым типы, А как был замечен по @dyp выше, есть также стандартные типы верстки которые, насколько я вижу, не обязательно перекрываются с тривиально копируемыми типами.
стандартный говорит:
1.8 объектная модель C++
(...)
5 (...) Объект тривиально копируемого или стандартного типа компоновки (3.9) должен занимать смежные байты памяти.
так вот, что я вижу здесь:
- стандарт ничего не говорит о нетривиально копируемых типах wrt.
memcpy
. (как уже упоминалось несколько раз здесь) - стандарт имеет отдельную концепцию для стандартных типов компоновки, которые занимают непрерывное хранилище.
- стандартный не явно разрешить или запретить использование
memcpy
на объектах стандартной компоновки, которые являются не Тривиально Копируемым.
так что это не кажется явно вызвал UB, но это, конечно, также не то, что называется неуказанному поведению, поэтому можно сделать вывод, что @underscore_d сделал в комментарий к принятому ответу:
(...) Вы не можете просто сказать: "Ну, это не был явно вызван как UB, поэтому он определен поведение!- вот к чему сводится эта нить. N3797 3.9 точки 2~3 не определяют, что memcpy делает для нетривиально-копируемого объекты, так (...) [t]шляпа в значительной степени функционально эквивалентно UB в моих глазах, поскольку оба бесполезны для написания надежного, т. е. портативного кода
I лично пришел бы к выводу, что это составляет UB, насколько портативность идет (о, эти оптимизаторы), но я думаю, что с некоторым хеджированием и знанием конкретной реализации это может сойти с рук. (Просто убедитесь, что оно того стоит.)
Примечание: я также думаю, что стандарт действительно должен явно включать семантику типа стандартного макета во все memcpy
беспорядок, потому что это действительный и полезный usecase, чтобы сделать побитовую копию нетривиально Копируемые объекты, но это к делу не относится.
ссылки: могу ли я использовать memcpy для записи в несколько смежных подобъектов стандартной компоновки?