C++ 포인터 캐스팅에 대해서 질문합니다.

tmal의 이미지

size_t qstrlen(const char* s) {
 
   int64_t* aligned_s = (int64_t*)s,
            read_data,
            bsf_result;
 
   // s가 8의 배수번지가 아님.
   if( (intptr_t)s & 7 ) {
       aligned_s      = (int64_t*)( (intptr_t)s & -8 );
       ptrdiff_t diff = (s-(char*)aligned_s)*8;
       read_data      = (*aligned_s)>>diff;
       read_data      = (read_data-0x0101010101010101) & ~read_data & 0x8080808080808080;
       read_data     &= ( (uint64_t)-1 >> diff );
       if(read_data) {
         bsf_result = __builtin_ctzl(read_data)/8;
         return bsf_result;
       }
       aligned_s++;
   }
   goto START;
 
 
   // 메인 루프.
   do {
       aligned_s++;
START:
       read_data = *aligned_s;
       read_data = (read_data-0x0101010101010101)
                 & ~read_data
                 & 0x8080808080808080;
   } while(!read_data);
​
   bsf_result = __builtin_ctzl(read_data)/8;
   return (size_t) ((char*)aligned_s-s) + bsf_result;
}

최근에 (read_data-0x0101010101010101) & ~read_data & 0x8080808080808080 으로, 멀티바이트에서 널문자를 찾는 방법을 알게 되어, 한번 구현해본 qstrlen 함수입니다. 64-bit 모드에서 돌리려고 만든 코드이고, 실행 환경은 x64, g++-7 7.4.0 c++17 를 사용했습니다.

저는 포인터가 각각의 타입에 대한 포인터들은 내부표현이 다르거나, 크기가 다를 수 있으며 정렬제한을 지키는 한해서 다른 타입을 가리키는 포인터로 서로 변환할 수 있다고 알고 있습니다.

위 코드에서는 포인터가 8의 배수의 번지를 담고 있는지를 확인하려고, (intptr_t) 타입으로 캐스팅해서 비트연산자를 사용했습니다. 그러고는 8의 배수가 아니라면, -8과 AND 연산을 해서 s보다 low address이면서 8의 배수가 되도록 값을 수정한 후 (int64_t*) 로 캐스팅을 했는데요. 여기서 궁금한 것이 있습니다.

x64에서는 데이터포인터가 64-bit 여서, char*과 int*의 크기가 같다고 생각하고 s를 (int64_t*)으로 캐스팅을 했는데요. 그렇다면, x86-64 기종이 아닌 다른 아키텍쳐에서 char*과 int*의 크기나 내부표현이 다르다고 한다면, 위 코드는 제대로 작동하는가 입니다.

예를 들어, sizeof(char*) > sizeof(int*) 이어서 캐스팅 시에 잘리게 된다 하면, int64_t* 포인터에다가 char*을 대입하는 것은 잘못된 코드가 되는 걸까요?(기존의 주소값이 잘림)

그리고, 반대로 크기가 작은 포인터를 큰 포인터로 변경한다고 할때, 아래와 같은 코드가 있을때:

  int64_t  a  = 10, 
          *b  = &a;
  char*    c   = (int64_t*)b;

만약, 포인터의 크기도 다른데, 내부표현까지 다르다면, (int64_t*)형 포인터인 b 를 크기가 더 큰 (char*) 포인터로 변경한다면, 포인터가 zero-extended 되고 두 포인터 간의 내부표현이 다르므로 결국에는 캐스팅한 b를 c에서는 제대로 읽을 수 없는 일이 발생하는 걸까요?( int64_t*와 char*의 내부표현이 달라서, &a가 int64_t* 포인터 형식일때와 char* 포인터 형식일때와 값이 다르다고 생각).

찾아보니, void*, char*이 generic pointer로 불리는 이유가, 일단 사용하면 알아서 캐스팅되고 다른 포인터값을 담을 수 있을 만큼 크기가 크며(byte addressing을 해야되서), 다시 원래 포인터로 되돌렸을 경우 원래 포인터값과 똑같다는 것을 보장하기 때문이라고 알고 있습니다.

하지만, 그렇다는 것은 generic pointer에 담았다가 다시 원래 포인터형으로 되돌려서 쓰는 것이 아닌, int*과 같이 char*보다 작을 수 있는 포인터를 char*로 캐스팅한 후 그대로 char*로 사용한다면 그 포인터가 제대로 작동할 수 있다는걸 보장하지 않는다는 건가요??.

int32_t  a;
int16_t* p = &a;

만약 그렇다면, 이렇게 하위 바이트들을 접근하는 코드는 이식성이 없다고 봐야하나요?( 맨 위의 qstrlen은 x86-64 기종에 종속적인 코드가 되는건가요?). c++ 포인터에 대해서 갑자기 헷갈려서 질문드립니다.

익명 사용자의 이미지

1. 이 정도로 C++의 디테일에 관심이 있는 사람이라면, 직접 찾아보는 게 빠르지 않겠어요?

이런 질문에 답변을 달아줄 수 있는 사람 중에, 인터넷에서 공짜 답변이나 달며 돌아다닐 만큼 할 일 없는 사람이 얼마나 될지.

2. 토끼굴을 파고들어 봅시다.

int64_t* aligned_s = (int64_t*)s

여기서 (int64_t*)s는 cast notation입니다. 속칭 C-style cast라고 부르는 표현이죠.

C++20 표준 7.6.3.4 항목에 따라 위 표현은 다음과 같습니다: const_cast<int64_t *>(reinterpret_cast<const int64_t *>(s))

Protip: C-style cast 사용은 지양하는 게 좋습니다. C++ 유저라면 제발 static/const/dynamic/reinterpret_cast를 직접 사용합시다.

현재 논의에서 불필요한 바깥쪽 const_cast는 무시하고, reinterpret_cast<const int64_t *>(s)만 살피도록 합시다.

표준 7.6.1.10.7에 따라 위 표현은 다음과 같습니다: static_cast<const int64_t *>(static_cast<const void *>(s))

표현식이 두 단계로 나뉘었습니다. 하나씩 살펴봅시다.

(1) static_cast<const void *>(s)

이 부분은 비교적 간단합니다. standard conversion sequence (표준 12.4.4.2.2) 중 하나인 pointer conversion (표준 7.3.12)이 적용되는 상황이지요.

표준 7.3.12.2 항목에 의해 "pointer to cv T" 타입의 prvalue는 "pointer to cv void" 타입의 prvalue로 변환되며, 이 때 포인터의 값은 변하지 않습니다.

표준 6.8.3.5 항목에 따라 "pointer to cv void" 타입은 어떤 object pointer도 담을 수 있다는 점도 언급하고 지나가야겠네요.

(2) static_cast<const int64_t *>(/* (1) */)

이 부분이 문제입니다.

표준 7.6.1.9.13에 따르면 "pointer to cv1 void" 타입의 prvalue는 static_cast에 의해 "pointer to cv2 T" 타입의 prvalue로 변환될 수 있습니다.

이미 만족하고 있는 몇 가지 자질구레한 규칙들 (cv2가 cv1보다 same or greater cv-qualified 되어야 한다거나)은 대충 넘어가고요.

여기서 문제는, 원본 포인터 값 A가 T의 alignment 조건을 맞추지 못한다면, 변환 결과가 unspecified 된다는 것입니다. 즉, 구현에 따라 다를 수 있습니다.

원래의 포인터 값, 즉 s는 const char * 타입이고, char는 narrow character type (표준 6.8.2.7)로서 가장 약한 alignment 조건을 가집니다 (표준 6.7.6.6).

따라서, 만약 int64_t가 조금이라도 더 강한 alignment 조건을 가진다면, 위 변환의 결과는 unspecified 입니다.

제 개발환경 중 하나에서 alignof(int64_t)를 찍어 보니 8이 나오는군요.

3. 그런데 실제로 문제가 되는 구현이 있는가?

제가 자칭 C++ nerd라고 떠들기는 합니다만, 다양한 C++ 구현 환경을 실제로 경험해 볼 기회가 있었던 것은 아닌지라 뭐라 답하기가 참 어렵군요.

x86_64처럼 모든 포인터 타입이 실상 word-size integer와 별 차이가 없고, align 상관 없이 접근은 어떻게든 되는 아키텍처라면 별 문제 없을 수 있겠지요.

혹시 문제가 되는 구현을 실제로 발견하신다면 제게도 알려 주세요.

tmal의 이미지

c++ 을 사용하는데, c-style 캐스팅이 편해서 계속 저렇게 쓰고 있네요.지적 감사드립니다. 답변내용에서 "만약 int64_t가 조금이라도 더 강한 alignment 조건을 가진다면, 위 변환의 결과는 unspecified 입니다" 라는 것은

(pointer to cv T) → (pointer to cv1 void) → (pointer to cv2 T)

처럼, 어떤 object 타입의 포인터든 담을 수 있는 pointer to cv void 타입에 담았다가 다시 원래 포인터 형으로 되돌리는 것은 괜찮지만(이런 경우를 A라고 하겠습니다), 그 이외의 포인터 타입으로 변경하는 것은 변환결과가 항상 unspecified 라고 생각하는 것이니, 이식성을 원한다면, A의 경우를 제외한 나머지 포인터 타입들로의 형변환은 사용하지 않는게 좋다 는 의미라고 생각하면 될까요?.

라스코니의 이미지

질문이 잘 이해가 안되는 점이 있네요. 그냥 코멘트를 드리자면,

1) 크기가 작은 포인터, 큰 포인터는 없습니다. 32 bit 시스템에서 포인터는 모두 32 bit 길이를 가지고, 64 bit 시스템에서는 64 bit 길이를 가집니다. 그래서 어떤 시스템에서든 int*와 char*는 같습니다. 다만 그 주소가 가리키는 대상의 사이즈가 다를 뿐입니다.

2) 각 시스템에서 short, int, long 이 가지는 길이는 각각 상이할 수 있습니다. 보통 이식성을 위해 #define int8 char 같은 것을 types.h 또는 common.h 같은 헤더파일에 각 OS 마다 따로 정의해서 씁니다. OSAL(Operating System Abstraction Layer)라고 찾아보시고 그 코드를 보시면 참고가 되실 겁니다.

3) 포인터 캐스팅시 주소 변환이 일어나는데 어떤 프로세서는 alignment를 따지는 것도 있습니다. 자기가 원하는 식으로 주소 정렬이 되지 않으면 뻣습니다. 가령 char는 모두 32 bit 주소로 align되어야 한다는 식입니다. 그렇게 되면 int 형 같은 것을 char로 짤라서 바로(?) 볼 수 없게 됩니다. 다행히 x86은 그런 것은 없는 것 같네요.

4) 이런 식으로 사용하는 것을 본 적이 없습니다.

  int64_t  a  = 10, 
          *b  = &a;
  char*    c  = (int64_t*)b;

반드시 char *c = (char *)b; 이런식으로 써야 됩니다. 어떤 대상이 가리키는 영역을 다른 크기(크던 작던)로 잘라서 볼 수 있도록 하는 것이 캐스팅이기 때문에 char a[]; int *c = (int *)a; 같은 식으로 사용해야 합니다.

5) 캐스팅해서 연산하는 것은 CPU로서는 이리 저리 짜르고 붙이고 하는 연산이 들어가서 더 무거울 수 있습니다. 보통은 원래 형태의 데이터를 가지고 뭔가를 하는 것이 가장 가볍습니다.

익명 사용자의 이미지

Quote:
1) 크기가 작은 포인터, 큰 포인터는 없습니다. 32 bit 시스템에서 포인터는 모두 32 bit 길이를 가지고, 64 bit 시스템에서는 64 bit 길이를 가집니다. 그래서 어떤 시스템에서든 int*와 char*는 같습니다. 다만 그 주소가 가리키는 대상의 사이즈가 다를 뿐입니다.

대부분의 경우에는 그렇겠습니다만, 그렇게 함부로 단정짓지는 않는 편이 좋겠습니다. :)

언어 표준이 보장하는 것은 이것뿐이지요.

sizeof(void *) = sizeof(char *)sizeof(/* 다른 모든 object pointer */)

오늘날 64bit의 넉넉하고 평평한 x86_64 메모리공간에서 일하는 프로그래머들은 이해하기 어려울지도 모르겠습니다만, C/C++에서 포인터를 이렇게 유연하게 정의한 데는 이유가 있을 겁니다.

https://stackoverflow.com/questions/399003/is-the-sizeofsome-pointer-always-equal-to-four

16비트 환경인데 32비트 far pointer로 20bit 주소 공간을 접근했던 x86 real mode의 추억이 떠오르는군요. :)

tmal의 이미지

char* c = (int64_t*)b; 이부분은 잘못쓴게 맞네요(이러면, 캐스팅하나마나 이네요). 답변을 보고 궁금한 것이 생겼는데, 질문 괜찮을까요?. 예를 들어, float에서 int로의 캐스팅의 경우, truncate 하여 int로 변환해주는 등, CPU가 변환 명령을 수행하는데, 그렇다면 포인터끼리의 캐스팅에서도 내부표현을 바꿔주는 등의 작업이 이루어질 수 있다는 의미인가요?. 즉, sizeof(int64_t*)==sizeof(char*) 라고 해도,

int64_t a;   // (int64_t*)&a != (char*)&a  

이런 형태가 나올 수 있다는 건가요?.

라스코니의 이미지

질문을 읽어보니 먼저 casting 할 수 있는 것을 구분해야 할 것 같네요.

1) type의 casting
int a = (int)f; float f = (float)a; 등이 가능하겠네요.
a's address = 0x98765432
f's address = 0x12345678 // 주소가 서로 다름

2) memory structure의 casting
float x;
structure { .... } x;
char x[100];

char *ptr = (char *)&x; 등이 가능할 것이고요.

실제 운용은 예를 들면 아래와 같이 보일 수 있을 겁니다.

int64_t a = 0x11223344;
//object      주소            값
//  a      0x12345678     0x11223344
int64_t *iptr = (int64_t*)&a;  // iptr's address = 0x12345678, *iptr = 0x11223344
short *sptr = (short*)&a;      // sptr's address = 0x12345678, *sptr = 0x1122
char *cptr = (char*)&a;        // cptr's address = 0x12345678, *cptr = 0x11
// 주소가 같으나 각 포인터 변수의 메모리 structure가 다름. 그래서 다른 값이 읽힘
라스코니의 이미지

좀 더 설명하자면 프로그램은 CPU+메모리 컨트롤러를 통해서 메모리라는 세상을 봅니다.

위에 코멘트가 달린 것처럼 그 메모리 세상은 매우 다양할 수 있습니다. 어떤 경우에는 far 포인터로 또는 메모리를 컨트롤러가 읽어서 알아서 해석하여 미국도 가게 해 주고, 한국도 가게 해주게 되죠. 16 bit 프로세서가 128 bit 주소, 256 bit 주소를 액세스하는 것도 가능한 이유가 바로 이 이유입니다.

그런 큰 영역을 액세스할 때는 그 영역에 대한 포인터 접근을, 일반적으로 CPU가 보여주는 영역을 액세스할 때는 그 영역에 맞는 포인터 접근을 사용합니다. 그래서 그 영역의 포인터가 가지는 형식이나 접근 방식은 반드시 같아야 한다고 적은 것입니다.

댓글 달기

Filtered HTML

  • 텍스트에 BBCode 태그를 사용할 수 있습니다. URL은 자동으로 링크 됩니다.
  • 사용할 수 있는 HTML 태그: <p><div><span><br><a><em><strong><del><ins><b><i><u><s><pre><code><cite><blockquote><ul><ol><li><dl><dt><dd><table><tr><td><th><thead><tbody><h1><h2><h3><h4><h5><h6><img><embed><object><param><hr>
  • 다음 태그를 이용하여 소스 코드 구문 강조를 할 수 있습니다: <code>, <blockcode>, <apache>, <applescript>, <autoconf>, <awk>, <bash>, <c>, <cpp>, <css>, <diff>, <drupal5>, <drupal6>, <gdb>, <html>, <html5>, <java>, <javascript>, <ldif>, <lua>, <make>, <mysql>, <perl>, <perl6>, <php>, <pgsql>, <proftpd>, <python>, <reg>, <spec>, <ruby>. 지원하는 태그 형식: <foo>, [foo].
  • web 주소와/이메일 주소를 클릭할 수 있는 링크로 자동으로 바꿉니다.

BBCode

  • 텍스트에 BBCode 태그를 사용할 수 있습니다. URL은 자동으로 링크 됩니다.
  • 다음 태그를 이용하여 소스 코드 구문 강조를 할 수 있습니다: <code>, <blockcode>, <apache>, <applescript>, <autoconf>, <awk>, <bash>, <c>, <cpp>, <css>, <diff>, <drupal5>, <drupal6>, <gdb>, <html>, <html5>, <java>, <javascript>, <ldif>, <lua>, <make>, <mysql>, <perl>, <perl6>, <php>, <pgsql>, <proftpd>, <python>, <reg>, <spec>, <ruby>. 지원하는 태그 형식: <foo>, [foo].
  • 사용할 수 있는 HTML 태그: <p><div><span><br><a><em><strong><del><ins><b><i><u><s><pre><code><cite><blockquote><ul><ol><li><dl><dt><dd><table><tr><td><th><thead><tbody><h1><h2><h3><h4><h5><h6><img><embed><object><param>
  • web 주소와/이메일 주소를 클릭할 수 있는 링크로 자동으로 바꿉니다.

Textile

  • 다음 태그를 이용하여 소스 코드 구문 강조를 할 수 있습니다: <code>, <blockcode>, <apache>, <applescript>, <autoconf>, <awk>, <bash>, <c>, <cpp>, <css>, <diff>, <drupal5>, <drupal6>, <gdb>, <html>, <html5>, <java>, <javascript>, <ldif>, <lua>, <make>, <mysql>, <perl>, <perl6>, <php>, <pgsql>, <proftpd>, <python>, <reg>, <spec>, <ruby>. 지원하는 태그 형식: <foo>, [foo].
  • You can use Textile markup to format text.
  • 사용할 수 있는 HTML 태그: <p><div><span><br><a><em><strong><del><ins><b><i><u><s><pre><code><cite><blockquote><ul><ol><li><dl><dt><dd><table><tr><td><th><thead><tbody><h1><h2><h3><h4><h5><h6><img><embed><object><param><hr>

Markdown

  • 다음 태그를 이용하여 소스 코드 구문 강조를 할 수 있습니다: <code>, <blockcode>, <apache>, <applescript>, <autoconf>, <awk>, <bash>, <c>, <cpp>, <css>, <diff>, <drupal5>, <drupal6>, <gdb>, <html>, <html5>, <java>, <javascript>, <ldif>, <lua>, <make>, <mysql>, <perl>, <perl6>, <php>, <pgsql>, <proftpd>, <python>, <reg>, <spec>, <ruby>. 지원하는 태그 형식: <foo>, [foo].
  • Quick Tips:
    • Two or more spaces at a line's end = Line break
    • Double returns = Paragraph
    • *Single asterisks* or _single underscores_ = Emphasis
    • **Double** or __double__ = Strong
    • This is [a link](http://the.link.example.com "The optional title text")
    For complete details on the Markdown syntax, see the Markdown documentation and Markdown Extra documentation for tables, footnotes, and more.
  • web 주소와/이메일 주소를 클릭할 수 있는 링크로 자동으로 바꿉니다.
  • 사용할 수 있는 HTML 태그: <p><div><span><br><a><em><strong><del><ins><b><i><u><s><pre><code><cite><blockquote><ul><ol><li><dl><dt><dd><table><tr><td><th><thead><tbody><h1><h2><h3><h4><h5><h6><img><embed><object><param><hr>

Plain text

  • HTML 태그를 사용할 수 없습니다.
  • web 주소와/이메일 주소를 클릭할 수 있는 링크로 자동으로 바꿉니다.
  • 줄과 단락은 자동으로 분리됩니다.
댓글 첨부 파일
이 댓글에 이미지나 파일을 업로드 합니다.
파일 크기는 8 MB보다 작아야 합니다.
허용할 파일 형식: txt pdf doc xls gif jpg jpeg mp3 png rar zip.
CAPTCHA
이것은 자동으로 스팸을 올리는 것을 막기 위해서 제공됩니다.