Programing

"memcpy"로 2D 어레이를 복사하는 것이 기술적으로 정의되지 않은 동작인가?

c10106 2022. 5. 20. 21:33
반응형

"memcpy"로 2D 어레이를 복사하는 것이 기술적으로 정의되지 않은 동작인가?

최근 질문에 대한 논평에서 흥미로운 토론이 벌어졌다.지금은 그곳의 언어가 C이지만, 다음과 같은 함수를 이용하여 다차원 배열의 요소에 접근할 때 정의되지 않은 행동을 구성하는 것이 무엇인지를 놓고 C++ 표준이 지정하는 것으로 논의가 표류해 왔다.std::memcpy.

하여 C++로 있다const가능한 경우:

#include <iostream>
#include <cstring>

void print(const int arr[][3], int n)
{
    for (int r = 0; r < 3; ++r) {
        for (int c = 0; c < n; ++c) {
            std::cout << arr[r][c] << " ";
        }
        std::cout << std::endl;
    }
}

int main()
{
    const int arr[3][3] = { {1, 2, 3}, {4, 5, 6}, {7, 8, 9} };
    int arr_copy[3][3];
    print(arr, 3);
    std::memcpy(arr_copy, arr, sizeof arr);
    print(arr_copy, 3);
    return 0;
}

그 쟁점은 에게 요청되어 있다.std::memcpy : : : arr논쟁은 첫 번째 것에 대한 포인터가 될 것이다.int[3]그래서, 논의의 한쪽(테드 링모 주)에 따르면, 그 토론이 언제인가.memcpy함수는 해당 서브 어레이의 세 번째 요소를 넘어 데이터에 액세스하며, 공식적으로 정의되지 않은 동작이 있다(그리고 목적지에도 동일한 동작이 적용된다).arr_copy).

그러나 토론의 다른 쪽(보통과 내가 구독하는)은 정의상 각 2D 어레이가 연속 메모리를 점유할 것이라는 논거를 사용한다.memcpy그저 그렇다void*그 장소들에 대한 조언들 (그리고 세 번째,size논쟁은 유효하다), 그렇다면 여기에 UB가 있을 수 없다.

다음은 토론과 가장 관련 있는 코멘트의 요약으로, 원래의 질문(강력한 내 질문에 대한 강조 표시)에서 "정리"가 발생할 경우:

여기엔 아웃바운드가 없는 것 같아. 은빛처럼memcpy의 대열에 종사하다.int,의 집합에 가 있다.int [3]s, 그리고 둘 다 연속적이어야 한다(그러나 100% 확신할 수는 없다). 참조 가능1

첫 번째 바이트를 복사하는 경우 범위 밖 액세스가 발생함arr[0][3]실제로 실패하는 것은 본 적이 없지만, C++에서는 UB를 가지고 있다 – Ted Lyngmo

하지만 더memcpy함수/호출이 어레이 인덱싱을 수행하지 않음 - 두 개만 제공됨void*한 곳에서 다른 곳으로 기억을 복사하고 포인터를 사용한다.– Adrian Mole

그것이 C에서 중요한지 확실히는 말할 수 없다.C++에서는 그렇지 않다.첫 번째에 포인터가 있다.int[3]그리고 그 범위를 벗어나는 접근은 UB를 가지고 있다.C++ 기준에서 예외는 찾지 못했다. – Ted Lyngmo

나는 그렇게 생각하지 않는다.arr[0][3]적용이 된다.그 논리로 보아, 나는 두 번째 것을 베끼는 것이intint을 배열하다.memcpyUB도 마찬가지일 겁니다 int [3]단지 의 유형이다.arrarr전체 바이트 수로는sizeof (int [3]) * 3것 – . 내가 뭔가를 놓치고 있는 것 같아:/ – mediocregetable1

문제를 해결할 수 있는 C++ 언어 변호사(가급적)가 있는가? (가급적) C++ 표준의 적절한 인용문으로?

또한, 특히 두 언어 표준이 다른 경우, C 표준의 관련 인용구가 도움이 될 수 있으므로 이 질문에 C 태그를 포함시켰다.

std::memcpy(arr_copy, arr, sizeof arr);(당신의 예)는 잘 정의되어 있다.

std::memcpy(arr_copy, arr[0], sizeof arr);반면에, 정의되지 않은 행동을 유발한다(적어도 C++에서는, C에 대해 완전히 확신할 수 없다).


다차원 배열은 배열의 1D 배열이다.내가 아는 한, 그들은 진정한 1D 어레이(즉, 어레이 유형이 아닌 요소를 가진 어레이)에 비해 특별한 대우를 받지 못한다.

1D 어레이의 예를 고려해 보십시오.

int a[3] = {1,2,3}, b[3];
std::memcpy(b, a, sizeof(int) * 3);

이것은 분명히 합법적이다.1

에 유의하십시오.memcpy배열의 첫 번째 요소에 대한 포인터를 수신하고 다른 요소에 액세스할 수 있다.

요소 유형은 이 예제의 타당성에 영향을 주지 않는다.2D 배열을 사용하면 요소 유형이int[N]보다는int, 그러나 타당성은 영향을 받지 않는다.

이제 다른 예를 생각해 보십시오.

int a[2][2] = {{1,2},{3,4}}, b[4];
std::memcpy(b, a[0], sizeof(int) * 4);
//             ^~~~

이것은 UB를2 유발한다. 왜냐하면 그 이후로memcpy의 첫 번째 요소에 대한 포인터가 주어진다.a[0], 그것은 오직 의 요소들에 접근할 수 있다.a[0](a[0][i])) 그리고 아니다.a[j][i].

그러나, 만약 당신이 나의 의견을 원한다면, 이것은 "동일한" 종류의 UB로, 실무에서 문제를 일으키지 않을 것 같다(그러나 항상 그렇듯이, UB는 가능하면 피해야 한다).



1 C++ 표준이 설명되지 않음memcpy, 그리고 대신 C 표준을 가리킨다.C 표준은 다소 허술한 표현을 사용한다.

C11(N1570)

memcpy복사하다.n에 의해 가리킨 사물에서 나온 문자.s2에 의해 지적된 대상에.s1.

배열의 첫 번째(또는 모든) 요소에 대한 포인터는 전체 배열이 해당 포인터를 통해 도달할 수 있더라도 전체 배열을 가리키지 않고 해당 요소만 가리킨다.따라서 문자 그대로 해석하면 @Language Lawyer가 옳다고 보인다: 만약 당신이 준다면.memcpy배열 요소에 대한 포인터, 단일한 요소만 복사할 수 있고 연속적인 요소는 허용되지 않는다.

이 해석은 상식과 모순되며, 아마도 의도된 것이 아닐 것이다.

예: 적용되는 의 예를 고려하십시오.memcpy첫 번째 요소에 대한 포인터를 사용하여 배열로: (예제가 비정규적이긴 하지만)

constexpr std::size_t N = sizeof(T);
char buf[N];
T obj;
std::memcpy(buf, &obj, N);
std::memcpy(&obj, buf, N);

2 이것은 moot이다. 왜냐하면 문제있는 단어 때문이다.memcpy상술한

나는 C에 대해 전적으로 확신하지는 않지만, C++에 대해서는 이것이 UB라는 강한 암시가 있다.

첫째로, 다음과 같은 예시를 생각해 보십시오.std::copy_n, 바이트가 아닌 요소별 복사를 수행하려고 시도:

#include <algorithm>

consteval void foo()
{
    int a[2][2] = {{1,2},{3,4}}, b[2][2] = {{1,2},{3,4}};
    std::copy_n(a[0], 4, b[0]);
}

int main() {foo();} 

컴파일 시간에 실행된 함수는 대부분의 형태의 UB(코드를 잘못 형성하게 함)를 포착하고, 실제로 이 코드 조각을 컴파일하면 다음과 같은 결과를 얻을 수 있다.

error: call to consteval function 'foo' is not a constant expression
note: cannot refer to element 4 of array of 2 elements in a constant expression

와의 상황.memcpy바이트 단위로 복사를 수행하기 때문에 덜 확실하다.이 전체 주제는 모호하고 구체적이지 못한 것 같다.

다음에 대한 표현을 고려하십시오.std::launder:

[ptr.launder]/4

저장 바이트b객체를 가리키는 포인터 값을 통해 도달할 수 있음Y목적이 있다면Z, 포인터 상호 변환 가능Y, 그런 것b다음이 차지하는 저장 공간 내에 있음Z, 또는 즉시 소멸되는 어레이 개체:Z배열 요소.

즉, 배열 요소에 대한 포인터가 주어지면 해당 배열의 모든 요소는 그 포인터를 통해(즉, 비반복적으로, 즉 ~를 통해) 도달할 수 있다.&a[0][0]단 한 가지a[0][i]도달할 수 있다.

공식적으로, 이 정의는 단지 설명하기 위해 사용된다.std::launder(주어진 포인터의 도달 가능한 영역을 확장할 수 없다는 사실)그러나 그 의미는 이 정의가 표준의 다른 부분에 의해 기술된 도달 가능성 규칙을 요약한 것으로 보인다([static.cast]/13통보한다).reinterpret_cast동일한 문구를 통해 정의된다. 또한 .

언급된 규칙이 에 적용되는지 확실하지 않다.memcpy하지만 그들은 그래야 한다.왜냐하면 그렇지 않으면 프로그래머는 라이브러리 기능을 사용하여 도달가능성을 무시할 수 있을 것이고, 이것은 도달가능성의 개념을 대부분 무용지물로 만들 것이기 때문이다.

사용해도 잘 정의되어 있다.memcpy(arr_cpy, arr, size)보다는
memcpy(&arr_cpy, &arr, size)(언어 변호사가 마침내 설명해 준 것은 그들이 줄곧 주장해 온 것이다), @HolyBlackCat 등의 설명에 의해 설명되는 이유 때문이다.

표준의 의도된 의미는 명확하며, 그 반대의 어떤 언어는 표준의 결함이지 컴파일러 데브가 캐스트하지 않는 memcpy(1D 어레이 포함)의 무수한 정상적인 사용 아래에서 융단을 꺼내기 위해 사용하는 것이 아니다.int*int (*)[N]특히 ISO C++는 가변 길이 어레이를 허용하지 않기 때문에 더욱 그렇다.

컴파일러 개발자가 표준 해석을 선택한 방법에 대한 실험적 증거로서, 밈프로피(memcpy)가 가리키는 전체 외부 객체(배열 배열)에서 읽을 수 있도록 하는 것으로, 밈프로피(memcpy)가 이 표준을 어떻게 해석했는지에 대한 실험 증거void*arg, 설령 그것이 그렇다 하더라도.void*첫 번째 요소(즉, 첫 번째 요소 배열):

너무 큰 사이즈를 통과하면 경고가 나오고, GCC의 경우 경고가 정확히 어떤 물체인지, 어떤 크기로 보이는지 철자까지 알려준다.memcpyed:

#include <cstring>

int dst[2][2];
void foo(){
    int arr[2][2] = {{1,1},{1,1}};
    std::memcpy(dst, arr, sizeof(arr));  // compiles cleanly
}

void size_too_large(){
    int arr[2][2] = {{1,1},{1,1}};
    std::memcpy(dst, arr, sizeof(arr)+4);
}

사용.&dst, &src경고나 경고의 부족에 대해서는 여기서 아무런 차이가 없다.
GCC 및 클랑용 Godbolt 컴파일러 탐색기-O2 -Wall -Wextra -pedantic -fsanitize=undefined, 및 MSVC-Wall.

GCC의 경고:size_too_large()다음과 같은 경우:

warning: 'void* memcpy(void*, const void*, size_t)' forming offset [16, 19] is  \
  out of the bounds [0, 16] of object 'dst' with type 'int [2][2]' [-Warray-bounds]
   11 |     std::memcpy(dst, arr, sizeof(arr)+4);
      |     ~~~~~~~~~~~^~~~~~~~~~~~~~~~~~~~~~~~~
<source>:3:5: note: 'dst' declared here
    3 | int dst[2][2];

clang은 개체 유형을 나타내지 않지만 여전히 크기를 나타낸다.

<source>:11:5: warning: 'memcpy' will always overflow; destination buffer has size 16, but size argument is 20 [-Wfortify-source]
    std::memcpy(dst, arr, sizeof(arr)+4);
    ^

그래서 그것은 우리가 이미 알고 있는 사실인 진짜 컴파일러와 함께 연습할 때 분명히 안전하다.둘 다 목적지 arg를 전체 16바이트 객체라고 본다.

그러나 GCC와 clang은 ISO C++ 표준보다 덜 엄격할 수 있다.와도dst[0]목적지로서.int*보다는int (*)[2](), 여전히 대상 크기를 유형으로 16바이트로 보고함int [2][2].

HolyBlackCat의 대답은 memcpy를 이렇게 부르면 실제로 전체 2D 배열이 아닌 2Element 하위 배열이 제공될 뿐이며 컴파일러는 더 큰 물체의 어떤 부분에 접근하기 위해 첫 번째 요소에 포인터를 사용하는 것에 대해 당신을 막거나 경고하려고 하지 않는다고 지적한다.

내가 말했듯이, 실제 컴파일러를 테스트하는 것은 이것이 현재 그들에게 잘 정의되어 있다는 것을 보여줄 수 있을 뿐이다; 그들이 미래에 무엇을 할지에 대한 논쟁은 다른 추론을 요구한다(memcpy의 정상적인 사용을 깨뜨리고자 하는 사람은 아무도 없고 표준의 의도된 의미에 근거함).


ISO 표준의 정확한 표현: 거의 틀림없이 결함

유일한 의문은 어떤 사물이 어떤 사물의 끝을 넘어 언어와 관련이 있는지를 설명하는 방법에 대한 표준의 문구에 결함이 있다는 주장에 장점이 있는지 여부, 그것이 배열 후 단일의 뾰족한 사물에 한정되어 있는지 여부, 아그를 멤피에 전달하기 위한 포인터 "decay"로 한정되어 있는지 여부다. (그리고 그렇다, 그것은 de de de de de de de depy가 될 것이다.)표준에 부합하다; 필요없고 사용해서는 안된다는 것이 널리 통설이다.&arrmemcpy 또는 기본적으로 AFAIK에 대한 어레이 유형 포함)

나에게는 그것이 표준에 대한 오해처럼 들리지만, 나는 물론 그것을 우리 모두가 알고 있는 것이 실제로 사실이라고 말하고 싶어서 편견을 가질 수도 있다.나는 여전히 그것을 잘 정의하고 있는 것이 표준의 문구에 대한 타당한 해석이라고 생각하지만, 다른 해석도 타당한 것일 수 있다.(즉, UB인지 아닌지 모호할 수 있는데, 결점이 될 수 있다.)

A void*배열의 첫 번째 요소를 가리키는 것은 에 다시 캐스팅될 수 있다.int (*)[2]전체 어레이 개체에 액세스하십시오.memcpy가 그렇게 사용하는 것은 아니지만, 그것은 포인터가 전체 N차원 배열의 포인터로서의 지위를 잃지 않았음을 보여준다.표준의 저자들이 이런 추론, 이런 것을 상정하고 있다고 생각한다.void*첫 번째 요소만이 아니라 전체 물체에 대한 포인터로 간주될 수 있다.

하지만, memcpy의 작동 방식에 특별한 언어가 있는 것은 사실이며, 공식적인 판독은 이것이 기억의 작동 방식에 대한 일반적인 C 가정에 의존하게 하지 않는다고 주장할 수 있다.

그러나 표준이 허용하는 UB의 해석은 그 표준이 어떻게 작동하기를 원하거나 그래야 한다고 생각하는 사람이 아니다.그리고 그것은 1D 어레이에 적용되므로, 이러한 해석은 잘 알려져 있거나 보편적으로 작동한다고 가정되는 memcpy를 사용하는 표준 사례와 상충된다.따라서 표준의 문구가 이것과 잘 맞지 않는다는 주장은 문구에 하자가 있다는 주장이지 코드를 바꿔서 이것을 피해야 한다는 주장이 아니다.

또한 컴파일러 개발자가 이 UB를 선언할 동기도 없다. 왜냐하면 여기에는 (서명된 오버플로, 형식 기반 앨리어싱 또는 NULL 디ref가 없다는 가정과 달리) 가질 최적화가 거의 없기 때문이다.

런타임 변수 크기가 캐스팅된 포인터 유형의 전체 첫 번째 요소에만 영향을 미쳐야 한다고 가정하는 컴파일러void*실제 코드로는 그리 최적화할 수 없을 겁니다컴파일러가 컴파일러를 작성하고자 하는 memcpy를 지나 계속적인 제안이나 유사한 일을 할 수 있도록 하는 것은 나중의 코드가 첫 번째 이후 요소에만 엄격하게 접근하는 경우는 드물다.

(내가 말했듯이, 모든 사람들은 이것이 표준이 의도한 것이 아니라는 것을 알고 있다. 서명 오버플로가 UB라는 것에 대한 명확한 진술과는 달리)

외람된 말씀이지만 홀리블랙캣은 완전히 틀렸다, 아주 첫 번째 원칙으로는.나의 C17 표준 초안은 7.24.1에서 "이 하위조항[memcpy 포함]의 모든 기능에 대해, 각 문자는 서명되지 않은 문자 형식을 가진 것처럼 해석되어야 한다."라고 말한다.C 표준은 이러한 사소한 기능인 memcpy copy memory에 대해 실제로 어떤 유형도 고려하지 않는다.의미론을 전혀 고려하지 않는 한, 서명되지 않은 문자의 배열로 취급된다.따라서 다음의 첫 번째 C 원칙이 적용된다.

주소에 초기화된 오브젝트가 있는 한, 당신은 문자 포인터를 통해 오브젝트에 접근할 수 있다.

강조와 명확성을 위해 반복합시다.

초기화된 모든 개체는 문자 포인터로 액세스할 수 있다.

예를 들어, 컴퓨터의 하드웨어가 마우스의 x 좌표를 매핑하기 때문에 개체가 특정 주소 0x42에 있다는 것을 알고 있다면, 당신은 그것을 문자 포인터로 변환하여 읽을 수 있다.좌표가 16비트 값이면 다음 바이트도 읽을 수 있다.

아무도 당신이 정수가 있다는 것을 어떻게 아는지 신경쓰지 않는다.만약 있다면 읽을 수 있다.(피터 코데스는 분절된 메모리 아키텍처가 있을 수 있기 때문에 관련이 없는 개체로부터 포인터 산술로 유효한 주소(또는 적어도 예상 주소에서)에 도달할 수 있다는 보장이 없다는 점에 주목했다.그러나 이것은 예시된 예가 아니다.전체 배열은 하나의 객체로서 단일 세그먼트에 상주해야 한다.)

이제 우리는 3개의 int로 구성된 3개의 배열을 가지고 있기 때문에 우리는 9 ints가 연속적으로 메모리에 저장된다는 것을 안다; 그것은 언어 요구 사항이다.그곳의 전체 기억은 하나의 물체에 속하는 ints로 가득 차 있고, 우리는 그 위에 수동으로 반복할 수도 있고, 또는 memcpy로 분산시킬 수도 있다.사용여부arr또는arr[0] 또는 다른 변수로부터 스택 오프셋을 통해 주소 가져오기 [<- 피터 코데스가 내게 일깨워준 것처럼 정확하다고 보장되지 않는다>나 다른 어떤 마술이나 단순히 교육받은 추측을 하는 것은 주소가 정확하다면 전적으로 무관하며, 여기에 의심의 여지가 없다.

그 질문은 C++에 관한 것이다; 나는 C에 대해서만 대답할 수 있다.C에서 이것은 잘 정의된 행동이다.나는 2020년 12월 11일 C2x 표준 초안을 인용할 것이다. http://www.open-std.org/jtc1/sc22/wg14/www/docs/n2596.pdf;에서 발견된 모든 강조 사항은 원본과 같을 것이다.

문제는 우리가 a를 적용할 수 있느냐 하는 것이다.memcpy완전히int[3][3]int[3][3]배열된 배열이지만memcpy바이트 단위로 작동한다.따라서 우리는 표준이 어레이를 바이트로 표현하는 것에 대해 무엇을 말하는지 알아야 할 것이다.

우리는 배열로 시작한다.6.2.5절, "유형" 22항은 배열 유형을 정의한다.

배열 유형은 요소 유형이라고 하는 특정 멤버 개체 유형을 사용하여 연속적으로 할당된 비어 있지 않은 개체 집합을 설명한다.

int[3][3]따라서 세 개의 비어 있지 않은 세트가 연속적으로 할당된다.int[3]물건들각각은 연속적으로 할당된 3개의 비빈 세트다.int물건들

먼저 에 대해 물어보자.int물건들모든 사람들은 a를 기대한다.memcpy단위의int일하기 위해표준이 이를 요구하는지 확인하기 위해 6.2.6.1, "일반" 제2항을 살펴본다.

비트 필드를 제외하고 오브젝트는 명시적으로 지정되거나 구현이 정의된 숫자, 순서 및 인코딩 등 하나 이상의 연속적인 시퀀스로 구성된다.

그래서 안int하나 이상의 바이트의 연속 시퀀스.그러므로 우리의int[3][3]3개의 연속된 연속된 연속된 연속된 연속된 연속된 연속된 연속된 연속된 연속된 연속된 연속된 연속된 연속의 연속된 연속된 연속이다sizeof(int)바이트; 표준은 9 ×이어야 함sizeof(int)연속 바이트

이 표준은 또한 이러한 바이트들이 배열 지수와 어떻게 관련되는지에 대한 요구사항을 제시한다.섹션 6.5.2.1 "어레이 첨자" 제2항은 다음과 같이 말한다.

사후 처리 식 다음에 대괄호 안의 식이 표시됨[]배열 객체의 요소를 첨자로 지정하는 것이다.첨자 연산자의 정의[]이다E1[E2]와 동일하다(*((E1)+(E2))).

그렇게arr[1] == *((arr)+(1))두번째다int[3],arr[1][2] == *((*((arr)+(1)))+(2))세 번째 원소야, 그리고 이게 여섯 번째 원소가 되어야 해.int의 시초를 지나서.arr제3항은 이에 대해 명백하다.

연속 첨자 연산자는 다차원 배열 객체의 요소를 지정한다.만약E치수 i × j × ···· k를 갖는 n차원 배열(n dimensions 2)이다.E(lvalue 이외의 값으로 사용)는 치수 j × ··· × k를 가진 (n - 1)차원 배열의 포인터로 변환된다. 단수일 경우*연산자는 명시적으로 또는 첨자의 결과로 암묵적으로 이 포인터에 적용되며, 그 결과 참조된 (n - 1)차원 배열이며, l값 이외의 값으로 사용될 경우 포인터로 변환된다.이에 따라 배열은 행 주 순서로 저장된다(마지막 첨자는 가장 빠르게 다름).

그럼에도 불구하고, 당신은 여전히 접속할 수 없다.arr[0][4]. Ted Lyngmo의 답안지로서 부록 J.2는 다음과 같이 구체적으로 말하고 있다.

lvalue 식과 같이 지정된 첨자로 개체에 액세스할 수 있는 것으로 보이는 경우에도 어레이 첨자가 범위를 벗어남a[1][7]선언에 따라int a[4][5]) (6.5.6).

하지만 그 이후로memcpy정말 바이트 정도야, 괜찮아소스와 대상이 다차원 어레이가 아니라void *. 7.24.2.1 "더memcpy기능"은 다음과 같이 설명한다.

memcpy복사하다.n에 의해 가리킨 사물에서 나온 문자.s2에 의해 지적된 대상에.s1.

"문자"는 제3.7절에 따라 세 가지 의미를 가질 수 있다.관련된 것은 「단일 바이트 문자」(3.7.1)인 것 같다.memcpy사본n바이트. 따라서memcpy(arr_copy, arr, sizeof(arr))반드시 베껴야 한다arrarr_copy바르게

생각해보니memcpy베낀다고는 말하지 않는다.n연속 바이트같은 바이트를 복사할 수 있을 것 같다.n아니면 고른다.n무작위 바이트그렇게 되면 디버깅은...흥미있는

"memcpy"로 2D 어레이를 복사하는 것이 기술적으로 정의되지 않은 동작인가?

(n.b, 이는 https://port70.net/~nsz/c/c11/n1570.107의 C11 표준 초안에 따라 C에만 해당됨)

아니, 그렇지 않아.

TLDR 요약:

6.7.6.3 기능 해제기(시제품 포함) 제7항은 기능 호출에서 포인터로 배열의 붕괴를 정의한다.그러나 그러한 붕괴는 6.9.1 기능 정의 제7항의 후원 하에 이루어진다. 이 정의는 "... 두 경우 모두 매개변수 유형 목록에 대해 6.7.6.3에 기술된 대로 각 매개변수의 유형이 조정된다. 결과 유형은 완전한 물체 유형이어야 한다."

배열이 함수 매개 변수로 전달될 때 배열에 의해 발생하는 포인터가 소멸한다는 개념을 직접적으로 반박하는 것이다.

자세한 답변

첫 번째 배열은 "완전한 개체"이다.

어레이가 "완전한 개체"여야 하는 이유

(배열을 "완전한 개체"로 정의하는 문구를 표준에서 찾을 수 있는 경우 이 답변의 전체 섹션은 중복된다.)

(도안) C11 표준에서와 같이 명시적으로 정의되지는 않지만(적어도 내가 찾을 수 있는 곳은 아님), 배열은 "완전한 객체" 범주에서 배열을 명시적으로 제거하는 문과 같이 여러 문장에서 암시적으로 "완전한 객체"이다.

6.5.2.2 기능 호출, 제1항:

호출된 함수를 나타내는 표현식은 보이드를 반환하거나 배열 형식이 아닌 완전한 객체 유형을 반환하는 기능을 위한 형식 포인터를 가져야 한다.

6.7.2.1 구조 조합 지정자문단 18의 "유연한 배열 구성원" 이외의 구조와 조합의 배열 구성원을 명시적으로 허용하지 않는다.

특별한 경우로서, 두 개 이상의 명명된 구성원을 가진 구조물의 마지막 요소는 불완전한 배열 유형을 가질 수 있다; 이것을 유연한 배열 구성원이라고 한다. ...

6.7.2.1 구조조합 지정자의 유일한 단락은 9항이다.

구조 또는 조합의 구성원은 가변적으로 수정된 형식 이외의 완전한 객체 형식을 가질 수 있다.

그것은 (도안) C11 표준에서 구조물과 유니언에 배열을 포함시킬 수 있는 유일한 진술이다.

어레이 초기화는 6.7.9 초기화 항목 3항에서 다룬다.

초기화할 엔터티의 유형은 알 수 없는 크기의 배열이거나 가변 길이 배열 유형이 아닌 완전한 개체 유형이어야 한다.

그것은 "완전한 객체" 범주를 통해서만 고정되고 알려진 크기의 배열만을 다룬다.

함수 반환 값에는 "완전한 개체" 범주에서 명시적으로 6.9.1 함수 정의, 제3항:

함수의 반환 형식은 보이드 또는 배열 형식 이외의 완전한 객체 형식이어야 한다.

그래서 우리는 어레이가 "완전한 객체"라는 것을 확립했다.

함수에 대한 매개 변수가 "완전한 개체 유형"임

6.9.1 기능 정의, 의미론, 항목 7:

각 매개변수의 형식은 매개변수 형식 목록에 대해 6.7.6.3에 설명된 대로 조정된다. 결과 형식은 완전한 객체 형식이어야 한다.

"완전한 객체"가 중요한 이유

6.5.2.1 배열 첨자, 제1항은 다음과 같이 기술한다.

표현식 중 하나는 "완료 대상 포인터" 형식이어야 하고, 다른 표현식은 정수 형식을 가져야 하며, 그 결과에는 "타입" 형식이 있어야 한다.

그리고 6.9.1p7에 따라 배열이 "완전한 개체 유형"으로 전달되었으며, 이는 전체 배열에 액세스하기 위해 포인터를 참조 해제할 수 있음을 의미한다.

Q.E.D.

의 명시적 사용법memcpy유용한 구성을 "파손된" 것으로 간주하기 위한 구실로 표준서를 남용하지 않는 편집자에 의해 의미 있게 처리될 것이다.기준서가 실제로 모순 없이 그것을 정의하는지 여부에 관심을 가져야 하는 유일한 사람들은 기준서를 남용하는 컴파일러 작성자들 또는 기준서를 남용하는 컴파일러 작성자들로부터 스스로를 보호하려는 사람들일 것이다.C나 C+++ 표준이 남용에 면역이 되도록 의도했다면 100% 불분명하게 모든 경우를 명시했는지에 대해 걱정할 필요가 있을 것이다.memcpy효과가 있을 거야그러나 두 가지 모두 컴파일러 작성자에 의존하여 작성되는데, 만약 기준서가 어떤 구성의 작동 방식을 동시에 명시하지만, 중복되는 구성 집합을 정의되지 않은 행동을 촉발하는 것으로 특징짓는다면, 컴파일러는 코드를 실용적으로 처리하기 위해 선의의 노력을 기울여야 한다는 것을 인식하기 위해 작성되었다.

다음 두 가지 기능을 고려하십시오.

char arr[4][4][4];

int test1(int i, unsigned mode)
{
  arr[1][0][0] = 1;
  memcpy(arr[0][i], arr[2][0], mode & 4);
  return arr[1][0][0];
}

int test2(int i, unsigned mode)
{
  arr[1][0][0] = 1;
  memcpy(arr[0]+i, arr[2], mode & 4);
  return arr[1][0][0];
}

프로그래머가 무엇을 하려고 하는가에 따라, 다음 해석 중 어느 것이든 가장 유용할 수 있다.

  1. 값을 다시 로드하는 방식으로 두 기능 모두 처리arr[1][0][0]의 뒤에memcpy.

  2. 두 기능 모두 1을 조건 없이 반환하는 방식으로 처리한다.memcpy그것을 덮어쓰다

  3. 첫 번째 기능은 무조건 1을 반환하는 방식으로 처리하되, 두 번째 기능은 다시 로드하는 방식으로 처리한다.arr[1][0][0]표준이 배열의 붕괴에 따른 배열 lvalue/glvalue 측면에서 색인 연산자의 사용을 정의하지만, 프로그래머의 구문 선택은 배열형 lvalue/glvalue가 실제로 배열로 사용되고 있는지 또는 첫 번째 요소에 대한 포인터를 얻는 수단으로 사용되고 있는지에 기초하는 경우가 많다. wh이후 주소 계산을 위한 기준으로 사용된다.

If a compiler were to attempt to process the code meaningfully in the case where i and mode are 4, there would be no bona fide ambiguity about how the code should behave. Only one behavior would make sense. The only ambiguity is whether the benefits of accommodating that case would be worth the execution cost of doing so; accommodating the behavior is always a "safe" choice. It would be awkward to write the Standard to say that test1 should have defined behavior for i==0..3 when n is 4, and i==0..4 when n is zero, but test2 should have defined behavior for i==0..15 regardless of n, but for most purposes the best blend of semantics, compatibility, and optimization would be achieved by processing code in that fashion.

My current standpoint is that when an int[3][3] is passed as an argument to a function, it decays into a pointer to the first element in that array. The first element is an int[3] and the other two int[3]s are in range - just like when you pass a 1D int[3] to a function, you get a pointer to the first int and the other two ints are in range, hence the memcpy is safe.


Original answer:

This answer is based on some wrong assumptions I made by reading something a long time ago. I'll leave the answer and the comments up to perhaps prevent other people from walking into the same mind-trap.

What is passed to the function decays into a pointers to the first elements, that is in this case, two int(*)[3]s.

C draft Annex J (informative) Portability issues J.2 Undefined behavior:

An array subscript is out of range, even if an object is apparently accessible with the given subscript (as in the lvalue expression a[1][7] given the declaration int a[4][5]) (6.5.6).

memcpy(arr_copy, arr, sizeof arr); get's two int(*)[3] and will access both out of range, hence, UB.

C++ standard says ([cstring.syn]/1):

The contents and meaning of the header <cstring> are the same as the C standard library header <string.h>.

C11 7.24.2.1 The memcpy function says:

Synopsis

1

         #include <string.h>
         void *memcpy(void * restrict s1,
              const void * restrict s2,
              size_t n);

Description

2 The memcpy function copies n characters from the object pointed to by s2 into the object pointed to by s1

Given this description, one may wonder what if n is greater than the size of the object pointed to by s1/s2. «상식 »는 복사가 예를 들어,sizeof(int)에서의 바이트 수int사물은 무의미해야 한다.

그리고 실제로 7.24.1 문자열 함수 규약 p.1은 다음과 같이 말하고 있다.

머리글<string.h>하나의 유형과 여러 함수를 선언하고, 문자 유형의 배열로 취급되는 문자 유형의 배열과 기타 객체를 조작하는 데 유용한 하나의 매크로를 정의한다.…배열 길이 결정에 다양한 방법을 사용하지만, 모든 경우 achar *또는void *인수는 배열의 초기(일반적으로 주소 지정) 문자를 가리킨다.객체의 끝을 넘어 어레이에 액세스하면 동작이 정의되지 않는다.

따라서 배열의 첫 번째 요소에 포인터를 전달하면 "개체 » 에서memcpyp.2 그리고 이 물체가 가지고 있는 것보다 더 많은 바이트를 복사하려고 하는 것은 UB이다.

참조URL: https://stackoverflow.com/questions/69329884/is-copying-2d-arrays-with-memcpy-technically-undefined-behaviour

반응형