TriviallyCopyable이 아닌 객체에 대해 std :: memcpy의 동작이 정의되지 않은 이유는 무엇입니까?
에서 http://en.cppreference.com/w/cpp/string/byte/memcpy :
객체가 TriviallyCopyable 이 아닌 경우 (예 : 스칼라, 배열, C 호환 구조체) 동작은 정의되지 않습니다.
내 작업에서 우리는 std::memcpyTriviallyCopyable이 아닌 객체를 비트 단위로 교체하는 데 오랫동안 사용했습니다.
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)는 새로운 배치와 같은 다른 방법으로 시작됩니다.
추신
@Cubbi의 댓글 :
@RSahu 무언가가 UB 다운 스트림을 보장하면 전체 프로그램을 정의되지 않은 것으로 렌더링합니다. 그러나 나는이 경우 UB 주위를 둘러 보는 것이 가능할 것으로 보이며 그에 따라 cppreference를 수정했습니다.
std::memcpy비 TriviallyCopyable 개체와 함께 사용할 때 자체 동작 이 정의되지 않은 이유는 무엇 입니까?
아니야! 그러나 간단하게 복사 할 수없는 유형의 한 객체의 기본 바이트를 해당 유형의 다른 객체로 복사 하면 대상 객체는 살아 있지 않습니다 . 우리는 저장소를 재사용하여 파괴했으며 생성자 호출로 다시 활성화하지 않았습니다.
대상 객체 사용 (멤버 함수 호출, 데이터 멤버 액세스)은 명확하게 정의되지 않은 [basic.life] / 6 이며, 자동 저장 기간을 갖는 대상 객체에 대한 후속 암시 적 소멸자 호출 [basic.life] / 4도 마찬가지 입니다. 정의되지 않은 동작이 소급 적이라는 점에 유의하십시오 . [intro.execution] / 5 :
그러나 그러한 실행에 정의되지 않은 작업이 포함되어있는 경우이 국제 표준은 해당 입력으로 해당 프로그램을 실행하는 구현에 대한 요구 사항을 지정 하지 않습니다 ( 첫 번째 정의되지 않은 작업 이전의 작업에 대해서도 ).
구현에서 객체가 어떻게 죽고 정의되지 않은 추가 작업의 대상이되는지를 발견하면 ... 프로그램 의미를 변경하여 반응 할 수 있습니다. 로부터 memcpy이후 전화. 그리고이 고려 사항은 최적화 프로그램과 그들이 만드는 특정 가정을 생각하면 매우 실용적입니다.
그러나 표준 라이브러리는 사소하게 복사 가능한 유형에 대해 특정 표준 라이브러리 알고리즘을 최적화 할 수 있고 허용 할 수 있습니다. std::copy사소하게 복사 가능한 유형에 대한 포인터에서 일반적으로 memcpy기본 바이트를 호출 합니다. 그래서 않습니다swap .
따라서 일반적인 일반 알고리즘을 사용하고 컴파일러가 적절한 저수준 최적화를 수행하도록하십시오. 이것은 부분적으로는 사소하게 복사 할 수있는 유형의 아이디어가 처음에 고안된 것입니다. 특정 최적화의 합법성 결정. 또한 이것은 언어의 모순되고 불특정 한 부분에 대해 걱정할 필요가 있기 때문에 뇌 손상을 방지합니다.
해당 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그런 객체는 그 불변을 깨뜨립니다.
GNU C ++ 11 std::string은 짧은 문자열로 정확하게이를 수행합니다.
이는 표준 파일 및 문자열 스트림이 구현되는 방식과 유사합니다. 스트림은 결국 std::basic_ios에 대한 포인터를 포함하는 파생 됩니다 std::basic_streambuf. 스트림에는 해당 포인터가 std::basic_ios가리키는 멤버 (또는 기본 클래스 하위 개체)로 특정 버퍼도 포함 됩니다.
표준이 그렇게 말하고 있기 때문입니다.
컴파일러는 비 TriviallyCopyable 유형이 복사 / 이동 생성자 / 할당 연산자를 통해서만 복사된다고 가정 할 수 있습니다. 이는 최적화 목적 일 수 있습니다 (일부 데이터가 비공개 인 경우 복사 / 이동이 발생할 때까지 설정을 연기 할 수 있음).
컴파일러는 memcpy전화 를 받아 아무것도하지 않거나 하드 드라이브를 포맷 할 수도 있습니다. 왜? 표준이 그렇게 말하고 있기 때문입니다. 그리고 아무것도하지 않는 것이 비트를 이동하는 것보다 확실히 빠르므로 memcpy동일하게 유효한 더 빠른 프로그램으로 최적화하지 않는 이유 는 무엇입니까?
이제 실제로는 예상하지 못한 유형의 비트를 블로 팅 할 때 발생할 수있는 많은 문제가 있습니다. 가상 기능 테이블이 올바르게 설정되지 않았을 수 있습니다. 누출 감지에 사용되는 기기가 올바르게 설정되지 않았을 수 있습니다. ID에 위치가 포함 된 객체는 코드에 의해 완전히 엉망이됩니다.
정말 재미있는 부분은 컴파일러에 의해 사소하게 복사 가능한 유형에 대해 using std::swap; swap(*ePtr1, *ePtr2);컴파일 될 수 있어야하고 memcpy다른 유형에 대해서는 정의 된 동작이 있어야한다는 것입니다. 컴파일러가 복사가 복사되는 비트라는 것을 증명할 수 있다면 memcpy. 더 최적의을 작성할 수 있다면 swap해당 객체의 네임 스페이스에서 작성할 수 있습니다.
C ++는 객체가 연속적인 스토리지 바이트를 차지한다고 모든 유형에 대해 보장하지 않습니다 [intro.object] / 5
사소하게 복사 가능하거나 표준 레이아웃 유형 (3.9)의 객체는 연속적인 저장 공간을 차지해야합니다.
실제로 가상 기본 클래스를 통해 주요 구현에서 인접하지 않은 개체를 만들 수 있습니다. 개체의 기본 클래스 하위 개체 x가 시작 주소 앞에x 있는 예제를 빌드하려고했습니다 . 이를 시각화하려면 다음 그래프 / 표를 고려하십시오. 가로 축은 주소 공간이고 세로 축은 상속 수준입니다 (수준 1은 수준 0에서 상 속됨). 로 표시된 필드 는 클래스의 직접 데이터 멤버 dm가 차지합니다 .
L | 00 08 16 -+ --------- 1 | dm 0 | dm
상속을 사용할 때 일반적인 메모리 레이아웃입니다. 그러나 가상 기본 클래스 하위 개체의 위치는 동일한 기본 클래스에서 가상으로 상속되는 자식 클래스에 의해 재배치 될 수 있으므로 고정되지 않습니다. 이로 인해 레벨 1 (기본 클래스 하위) 오브젝트가 주소 8에서 시작하고 크기가 16 바이트라고보고하는 상황이 발생할 수 있습니다. 이 두 숫자를 순진하게 더하면 실제로 [0, 16)을 차지하더라도 주소 공간 [8, 24)을 차지한다고 생각할 것입니다.
이러한 레벨 1 객체를 생성 할 수 있다면 memcpy복사에 사용할 수 없습니다 . memcpy이 객체에 속하지 않는 메모리에 액세스합니다 (주소 16 ~ 24). 내 데모에서 clang ++의 주소 새니 타이 저에 의해 스택 버퍼 오버플로로 잡혔습니다.
그러한 개체를 만드는 방법은 무엇입니까? 다중 가상 상속을 사용하여 다음과 같은 메모리 레이아웃 (가상 테이블 포인터가으로 표시됨 vp) 을 가진 객체를 생각해 냈습니다 . 다음과 같은 4 개의 상속 계층을 통해 구성됩니다.
L 00 08 16 24 32 40 48 3dm 2 vp dm 1vp dm 0dm
위에서 설명한 문제는 수준 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 :: 오프셋 0에서보고; 데이터는 오프셋 16에 있습니다. 순진하게 48 오프셋 l2 :: 오프셋 0에서보고; 데이터는 오프셋 8에 있습니다. 순진하게 40 오프셋 l1 :: 오프셋 32에서보고; 데이터는 오프셋 40에 있습니다. 순진하게 56 오프셋 l0 :: 오프셋 24에서보고; 데이터는 오프셋 24에 있습니다. 순진하게 32 오프셋 완전한 객체는 [0x9f0, 0xa20)을 차지합니다. [0xa10, 0xa28)에서 [0xa20, 0xa38)로 복사
두 개의 강조된 끝 오프셋에 유의하십시오.
이러한 답변 중 상당수는 memcpy나중에 정의되지 않은 동작을 유발할 수있는 클래스의 불변성을 깨뜨릴 수 있다고 언급하지만 (대부분의 경우 위험을 감수 할 수있는 충분한 이유가되어야 함) 실제로 요구하는 것 같지 않습니다.
memcpy호출 자체가 정의되지 않은 동작으로 간주되는 한 가지 이유 는 대상 플랫폼을 기반으로 최적화를 수행 할 수 있도록 컴파일러에 가능한 한 많은 공간을 제공하기 위해서입니다. 호출 자체가 UB가 됨으로써 컴파일러는 이상하고 플랫폼에 의존하는 작업을 수행 할 수 있습니다.
이 (매우 인위적이고 가상적인) 예를 고려하십시오. 특정 하드웨어 플랫폼의 경우 여러 종류의 메모리가있을 수 있으며 일부는 다른 작업에 대해 다른 메모리보다 빠릅니다. 예를 들어 더 빠른 메모리 복사를 허용하는 일종의 특수 메모리가있을 수 있습니다. 따라서이 (가상) 플랫폼 용 컴파일러 TriviallyCopyable는이 특수 메모리에 모든 유형 을 배치 memcpy하고이 메모리에서만 작동하는 특수 하드웨어 명령어를 사용하도록 구현할 수 있습니다.
이 플랫폼의 memcpy비 TriviallyCopyable객체에서 사용 하는 경우 memcpy호출 자체 에 낮은 수준의 INVALID OPCODE 충돌이있을 수 있습니다 .
아마도 가장 설득력있는 주장은 아니지만, 요점은 표준 이 그것을 금지하지 않는다는 것입니다 . 이것은 UB memcpy 호출을 통해서만 가능합니다 .
memcpy는 모든 바이트를 복사하거나 귀하의 경우 모든 바이트를 교체합니다. 지나치게 열성적인 컴파일러는 모든 종류의 장난에 대한 핑계로 "정의되지 않은 동작"을 취할 수 있지만 대부분의 컴파일러는 그렇게하지 않습니다. 그래도 가능합니다.
However, after these bytes are copied, the object that you copied them to may not be a valid object anymore. Simple case is a string implementation where large strings allocate memory, but small strings just use a part of the string object to hold characters, and keep a pointer to that. The pointer will obviously point to the other object, so things will be wrong. Another example I have seen was a class with data that was used in very few instances only, so that data was kept in a database with the address of the object as a key.
Now if your instances contain a mutex for example, I would think that moving that around could be a major problem.
Another reason that memcpy is UB (apart from what has been mentioned in the other answers - it might break invariants later on) is that it is very hard for the standard to say exactly what would happen.
For non-trivial types, the standard says very little about how the object is laid out in memory, in which order the members are placed, where the vtable pointer is, what the padding should be, etc. The compiler has huge amounts of freedom in deciding this.
As a result, even if the standard wanted to allow memcpy in these "safe" situations, it would be impossible to state what situations are safe and which aren't, or when exactly the real UB would be triggered for unsafe cases.
I suppose that you could argue that the effects should be implementation-defined or unspecified, but I'd personally feel that would be both digging a bit too deep into platform specifics and giving a little bit too much legitimacy to something that in the general case is rather unsafe.
First, note that it is unquestionable that all memory for mutable C/C++ objects has to be un-typed, un-specialized, usable for any mutable object. (I guess the memory for global const variables could hypothetically be typed, there is just no point with such hyper complication for such tiny corner case.) Unlike Java, C++ has no typed allocation of a dynamic object: new Class(args) in Java is a typed object creation: creation an object of a well defined type, that might live in typed memory. On the other hand, the C++ expression new Class(args) is just a thin typing wrapper around type-less memory allocation, equivalent with new (operator new(sizeof(Class)) Class(args): the object is created in "neutral memory". Changing that would mean changing a very big part of C++.
Forbidding the bit copy operation (whether done by memcpy or the equivalent user defined byte by byte copy) on some type gives a lot freedom to the implementation for polymorphic classes (those with virtual functions), and other so called "virtual classes" (not a standard term), that is the classes that use the virtual keyword.
The implementation of polymorphic classes could use a global associative map of addresses which associate the address of a polymorphic object and its virtual functions. I believe that was an option seriously considered during the design of the first iterations C++ language (or even "C with classes"). That map of polymorphic objects might use special CPU features and special associative memory (such features aren't exposed to the C++ user).
Of course we know that all practical implementations of virtual functions use vtables (a constant record describing all dynamic aspects of a class) and put a vptr (vtable pointer) in each polymorphic base class subobject, as that approach is extremely simple to implement (at least for the simplest cases) and very efficient. There is no global registry of polymorphic objects in any real world implementation except possibly in debug mode (I don't know such debug mode).
The C++ standard made the lack of global registry somewhat official by saying that you can skip the destructor call when you reuse the memory of an object, as long as you don't depend on the "side effects" of that destructor call. (I believe that means that the "side effects" are user created, that is the body of the destructor, not implementation created, as automatically done to the destructor by the implementation.)
Because in practice in all implementations, the compiler just uses vptr (pointer to vtables) hidden members, and these hidden members will be copied properly bymemcpy; as if you did a plain member-wise copy of the C struct representing the polymorphic class (with all its hidden members). Bit-wise copies, or complete C struct members-wise copies (the complete C struct includes hidden members) will behave exactly as a constructor call (as done by placement new), so all you have to do it let the compiler think you might have called placement new. If you do a strongly external function call (a call to a function that cannot be inlined and whose implementation cannot be examined by the compiler, like a call to a function defined in a dynamically loaded code unit, or a system call), then the compiler will just assume that such constructors could have been called by the code it cannot examine. Thus the behavior of memcpy here is defined not by the language standard, but by the compiler ABI (Application Binary Interface). The behavior of a strongly external function call is defined by the ABI, not just by the language standard. A call to a potentially inlinable function is defined by the language as its definition can be seen (either during compiler or during link time global optimization).
So in practice, given appropriate "compiler fences" (such as a call to an external function, or just asm("")), you can memcpy classes that only use virtual functions.
Of course, you have to be allowed by the language semantic to do such placement new when you do a memcpy: you cannot willy-nilly redefine the dynamic type of an existing object and pretend you have not simply wrecked the old object. If you have a non const global, static, automatic, member subobject, array subobject, you can overwrite it and put another, unrelated object there; but if the dynamic type is different, you cannot pretend that it's still the same object or 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
}
The change of polymorphic type of an existing object is simply not allowed: the new object has no relation with a except for the region of memory: the continuous bytes starting at &a. They have different types.
[The standard is strongly divided on whether *&a can be used (in typical flat memory machines) or (A&)(char&)a (in any case) to refer to the new object. Compiler writers are not divided: you should not do it. This a deep defect in C++, perhaps the deepest and most troubling.]
But you cannot in portable code perform bitwise copy of classes that use virtual inheritance, as some implementations implement those classes with pointers to the virtual base subobjects: these pointers that were properly initialized by the constructor of the most derived object would have their value copied by memcpy (like a plain member wise copy of the C struct representing the class with all its hidden members) and wouldn't point the subobject of the derived object!
Other ABI use address offsets to locate these base subobjects; they depend only on the type of the most derived object, like final overriders and typeid, and thus can be stored in the vtable. On these implementation, memcpy will work as guaranteed by the ABI (with the above limitation on changing the type of an existing object).
In either case, it is entirely an object representation issue, that is, an ABI issue.
What I can perceive here is that -- for some practical applications -- the C++ Standard may be to restrictive, or rather, not permittive enough.
As shown in other answers memcpy breaks down quickly for "complicated" types, but IMHO, it actually should work for Standard Layout Types as long as the memcpy doesn't break what the defined copy-operations and destructor of the Standard Layout type do. (Note that a even TC class is allowed to have a non-trivial constructor.) The standard only explicitly calls out TC types wrt. this, however.
A recent draft quote (N3797):
3.9 Types
...
2 For any object (other than a base-class subobject) of trivially copyable type T, whether or not the object holds a valid value of type T, the underlying bytes (1.7) making up the object can be copied into an array of char or unsigned char. If the content of the array of char or unsigned char is copied back into the object, the object shall subsequently hold its original value. [ Example:
#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 example ]
3 For any trivially copyable type T, if two pointers to T point to distinct T objects obj1 and obj2, where neither obj1 nor obj2 is a base-class subobject, if the underlying bytes (1.7) making up obj1 are copied into obj2, obj2 shall subsequently hold the same value as obj1. [ Example:
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 example ]
The standard here talks about trivially copyable types, but as was observed by @dyp above, there are also standard layout types that do not, as far as I can see, necessarily overlap with Trivially Copyable types.
The standard says:
1.8 The C++ object model
(...)
5 (...) An object of trivially copyable or standard-layout type (3.9) shall occupy contiguous bytes of storage.
So what I see here is that:
- The standard says nothing about non Trivially Copyable types wrt.
memcpy. (as already mentioned several times here) - The standard has a separate concept for Standard Layout types that occupy contiguous storage.
- The standard does not explicitly allow nor disallow using
memcpyon objects of Standard Layout that are not Trivially Copyable.
So it does not seem to be explicitly called out UB, but it certainly also isn't what is referred to as unspecified behavior, so one could conclude what @underscore_d did in the comment to the accepted answer:
(...) You can't just say "well, it wasn't explicitly called out as UB, therefore it's defined behaviour!", which is what this thread seems to amount to. N3797 3.9 points 2~3 do not define what memcpy does for non-trivially-copyable objects, so (...) [t]hat's pretty much functionally equivalent to UB in my eyes as both are useless for writing reliable, i.e. portable code
I personally would conclude that it amounts to UB as far as portability goes (oh, those optimizers), but I think that with some hedging and knowledge of the concrete implementation, one can get away with it. (Just make sure it's worth the trouble.)
Side Note: I also think that the standard really should explicitly incorporate Standard Layout type semantics into the whole memcpy mess, because it's a valid and useful usecase to do bitwise copy of non Trivially Copyable objects, but that's beside the point here.
Link: Can I use memcpy to write to multiple adjacent Standard Layout sub-objects?
'Nice programing' 카테고리의 다른 글
| 셸에서 셀러리 주기적 작업을 수동으로 실행하려면 어떻게해야합니까? (0) | 2020.11.11 |
|---|---|
| “RangeError : 최대 호출 스택 크기를 초과했습니다.”이유는 무엇입니까? (0) | 2020.11.11 |
| PowerShell을 시작하는 방법은 무엇입니까? (0) | 2020.11.11 |
| 다음 코드에서 sys.sp_addextendedproperty의 사용을 설명 할 수 있습니까? (0) | 2020.11.11 |
| 사용되지 않는 단어는 fill_parent와 match_parent의 유일한 차이점입니다. (0) | 2020.11.11 |