C 구조체 관련한 질문입니다.

twinwings의 이미지

typedef struct _Sample
{
    int dummy_data;
    int real_data[CONST_NUMBER];
} Sample;

이렇게 생긴 구조체가 있습니다.

real_data[-1]의 주소 값이 항상 dummy_data의 주소 값과 동일한가요?

즉, 플랫폼에 관계없이 항상 보장하나요? 만약 보장하지 않는다면

__attribute__((packed))

으로 해결할 수 있나요? 이것도 영 찝찝한데...

익명 사용자의 이미지

미묘한 문제로군요. -_-;;

여기에는 최소 두 가지 문제가 엮여 있습니다.

1. dummy_data와 real_data가 바로 잇달아 있을 것인가?

C언어 표준은 struct의 멤버가 그 순서대로 주소공간에 할당되어야 한다고 규정하고 있으며, 구조체 변수의 주소는 구조체 첫 멤버의 주소와 같아야 한다고 규정하고 있습니다.
즉 구조체의 맨 앞에는 패딩 바이트가 있을 수 없습니다.

바꿔 말하면, 구조체의 멤버와 멤버 사이 및 마지막 멤버 뒤에는 패딩 바이트가 있을 수 있습니다.
아키텍처에 따라, 컴파일러에 따라 최적화를 위해 dummy_data와 real_data 사이에 패딩 바이트가 들어갈 가능성을 배제할 수 없습니다.
int는 4바이트인데 CPU word size는 8바이트인 환경이라던가 말이죠.

__attribute__((packed))를 쓰면 패딩 바이트를 없앨 수 있냐고요? gcc 매뉴얼은 이렇게 말하는군요.

This attribute, attached to struct or union type definition, specifies that each member (other than zero-width bit-fields) of the structure or union is placed to minimize the memory required. When attached to an enum definition, it indicates that the smallest integral type should be used.

네. minimize the memory required이래요. 최소화한다고만 했지 패딩을 아예 없앤다고 보장해 주지는 않는군요.
그게 그 말일까요? int와 int 사이에 패딩 바이트를 정말로 없앨 수 없는 경우가 과연 있기는 할까요?
솔직히 좀 헷갈립니다.. 뭐 예를 들어서 int 타입인 real_data[0]와 real_data[1] (CONST_NUMBER >= 2일 때)는 반드시 잇달아 배치되어야 하기 때문에(표준은 배열 요소들이 반드시 패딩 없이 잇달아 배치될 것을 규정합니다.) 결국은 컴파일러가 마음만 먹으면 두 int를 잇달아 붙일 수 있기는 하지 않을까 싶거든요. 이 부분을 다시 말하면, int의 alignment 최소 단위가 sizeof(int)의 약수가 되어야 하는 거죠. 그러니 이 경우에 패딩이 없어질 거라고 가정해도 안전할 것 같은데, 다른 분들 의견도 듣고 싶네요.

2. real_data[-1] 같은 이상한 표현식이 과연 허용되는가.
무슨 ioccc 출품작 같은 데서나 쓸 법한 표현식이네요.
1번 문제는 플랫폼간 호환성 관련하여 표준 및 컴파일러 옵션이 어디까지 보장해주느냐에 대한 문제였다면, 이 문제는 표준이 이런 이상한 표현식에 대해서 어떤 것을 보장해주느냐에 대한 문제입니다.

천천히 차근차근 가죠. Array subscripting expression인 real_data[-1]은 표준에 의해 (*((real_data)+(-1)))과 동일한 것으로 정의됩니다.
페이지를 쭉 뒤로 넘겨서 Additive operators부터 먼저 살펴봐야겠네요. 아 맞다, 그 전에, 배열 이름인 real_data는 이 문맥에서 rvalue인 &real_data[0]으로 암시적 변환이 먼저 이루어집니다.
그러니 Additive operators에서 "포인터 + 정수"일 때의 규정에 대해서 살펴봐야겠네요.

When an expression that has integer type is added to or subtracted from a pointer, the result has the type of the pointer operand. If the pointer operand points to an element of an array object, and the array is large enough, the result points to an element offset from the original element such that the difference of the subscripts of the resulting and original array elements equals the integer expression. In other words, if the expression P points to the i-th element of an array object, the expressions (P)+N (equivalently, N+(P)) and (P)-N (where N has the value n) point to, respectively, the i+n-th and i−n-th elements of the array object, provided they exist. Moreover, if the expression P points to the last element of an array object, the expression (P)+1 points one past the last element of the array object, and if the expression Q points one past the last element of an array object, the expression (Q)-1 points to the last element of the array object. If both the pointer operand and the result point to elements of the same array object, or one past the last element of the array object, the evaluation shall not produce an overflow; otherwise, the behavior is undefined. If the result points one past the last element of the array object, it shall not be used as the operand of a unary * operator that is evaluated.

아 길다. -_-;; 대충 번역할게요.

정수 타입이 포인터 타입에 더해지거나 빼지는 표현식의 경우, 그 결과는 포인터 피연산자의 타입을 갖는다. 만약 포인터 피연산자가 어떤 배열의 요소 하나를 가리키고 있으며 그 배열이 충분히 크다면, 표현식의 결과 역시 그 배열의 요소이며, 표현식 결과가 가리키는 요소의 배열 인덱스와 원래 가리켜지던 요소의 배열 인덱스의 차는 정수 피연산자와 같다. 다시 말하면, 표현식 P가 어떤 배열의 i번째 요소를 가리키고 있었다면, 표현식 (P)+N(N+(P)도 같음)와 표현식 (P)-N (표현식 N의 값은 n)은 각각 배열의 i+n번째, i-n번째 요소가 존재할 때 그것들을 가리키게 된다. (후략)

번역 퀄리티가 썩 좋지 않아서 아마 집중해서 읽으려고 하셔도 잘 안될거에요. 그러니 짧고 간결하게 요약드리면, "배열 요소를 가리키는 포인터에 정수를 더하거나 빼서 그 배열의 영역을 벗어나게 만드는 수식"은 그 자체로 undefined behavior입니다. 와 이건 뭐 더 고려할 것조차도 없네요. 제가 영문으로 인용한 부분을 끝까지 읽으셨다면 배열의 끝부분에 딱 한 칸만큼 여유를 주긴 한다는 걸 아시게 될 텐데, 그 여유분조차도 * 연산자로 역참조 하면 안 됩니다.

더 짧게 말씀드리면, real_data[-1] 같은 건 얄짤없이 undefined behavior라는 거에요. 역참조까지 갈 것도 없습니다. (real_data - 1)까지만 해도 undefined behavior에요.
x86처럼 포인터 연산이 자유로운 아키텍처에서는 이런 걸 걱정할 필요 없지만, 진정으로 호환성을 고려하고자 하는 프로그래머라면 언어 표준이 대놓고 하지 말라는 짓을 하면 안 되겠죠.

twinwings의 이미지

우선 답변 감사드립니다.

인용하시는 부분의 "undefined behavior"는

If the result points one past the last element of the array object, it shall not be used as the operand of a unary * operator that is evaluated.

단순히 배열 끝을 넘어갈 때(전이든 후든)의 미정의 사항을 경고하는군요.

만약 real_data[-1]가 dummy_data를 가르키는 상황에서는 미정의 동작이 일어나지 않을 것 같습니다. 그게 바로 제가 원하는 것이니까요.

=====
플랫폼 관계없이 코딩하는건 사실상 불가능할 것 같아서

컴파일 타임 ASSERT 이용해서 구현했습니다.

C11에서는 static_assert 라는 이름으로 지원하네요.(assert.h)

C11 이전버전에서는 다음과 같이 트리키한 방법을 이용해서 구현하네요.

http://stackoverflow.com/questions/807244/c-compiler-asserts-how-to-implement

CASSERT(offsetof(Sample, dummy_data) + sizeof(dummy_data) == offsetof(Sample, real_data));

특정 플랫폼에 가정하고 코드 작성하고, 그 외의 플랫폼에서는 컴파일 안되도록

꼼수를 넣었습니다. 저에겐 이게 최선인 것 같네요.

dkim의 이미지

C 표준은 포인터 연산의 결과가 포인터 피연산자와 동일한 배열의 원소 혹은 그 배열의 마지막 원소 다음 위치를 가리키기를 요구하고 있습니다. real_data - 1real_data가 가리키는 배열의 원소도 마지막 원소 다음 위치도 가리키지 않으므로, C 표준 하에서 정의되지 않는 것으로 보는 게 맞을 듯합니다.

If both the pointer operand and the result point to elements of the same array object, or one past the last element of the array object, the evaluation shall not produce an overflow; otherwise, the behavior is undefined.

=====

여전히 찜찜하시면, 하나의 배열에 두 데이터를 모두 저장하고 접근자(accessor)를 매크로 혹은 (인라인) 함수로 정의해서 사용하는 방식도 고려해볼 수 있지 않을까 싶습니다.

#define CONST_NUMBER 10
 
#define SAMPLE_REAL_DATA(sample) (((sample).data) + 1)
 
typedef struct _Sample {
    int data[CONST_NUMBER + 1];
} Sample;
 
int main() {
    Sample s = {{0,}};
 
    SAMPLE_REAL_DATA(s)[-1] = -1;
}
익명 사용자의 이미지

역시나, 제가 너무 장황하게 쓴 탓에 오해가 벌어지는군요. dkim님의 말씀이 맞습니다. real_data - 1는 미정의 동작을 일으킵니다.

이런 대안도 있겠네요

typedef struct _Sample
{
    union{
        int dummy_data;
        int real_data[CONST_NUMBER + 1];
    } data;
} Sample;

이제 &real_data[0] == &dummy_data입니다. 인덱스가 한 칸씩 밀리는 문제는 있습니다만.
HDNua의 이미지

잘 모르고 하는 이야기일지 모르나 의견 남겨봅니다. 위에 익명 사용자님께서 유니온을 이용한 방법을 말씀해주셨기에...

typedef struct _Sample {
    union {
        int _data[CONST_NUMBER+1];
        struct {
            int dummy_data;
            int real_data[CONST_NUMBER];
        } data;
    } data;
} Sample;
#define SAMPLE_DUMMY(SAMPLE_OBJECT) ((SAMPLE_OBJECT).data.data.dummy_data)
#define SAMPLE_RDATA(SAMPLE_OBJECT) ((SAMPLE_OBJECT).data.data.real_data)
#define SAMPLEP_DUMMY(SAMPLE_OBJECT) ((SAMPLE_OBJECT)->data.data.dummy_data)
#define SAMPLEP_RDATA(SAMPLE_OBJECT) ((SAMPLE_OBJECT)->data.data.real_data)

사람이 생각할 수 있는 가장 한심한 방법 중의 하나이긴 한데, 인덱스 문제는 해결되는군요. ^^

저는 이렇게 생각했습니다.

익명 사용자의 이미지

이것도 좋은 방법이긴 한데, __attribute__((packed))를 쓰든 뭘 쓰든 해서 dummy_data와 real_data가 패딩 없이 잇달아 배치되도록 보장해 줄 필요가 있습니다.
첩첩산중이죠. 컴파일러를 gcc로 한정한다고 해도 이게 항상 가능한지에 대해서 아직 확신이 없습니다. 가능할 거라는 개연성은 충분히 있는데...

댓글 달기

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
이것은 자동으로 스팸을 올리는 것을 막기 위해서 제공됩니다.