Media Log

_countof 매크로

2011.03.15 06:48 | Programming
_countof 매크로는 배열의 원소 개수를 돌려주는 서비스 매크로이다. 비주얼 스튜디오 2005 부터인가 제공되었던 것 같다.
StringCchCopystrcpy_s, wcscpy_s 같은 함수들을 사용할 때 편리하게 사용할 수 있다.

_countof 매크로는 다음과 같이 생겼다.
#if !defined(_countof)
#if !defined(__cplusplus)
#define _countof(_Array) (sizeof(_Array) / sizeof(_Array[0]))
#else
extern "C++"
{
template <typename T, size_t N>
char (*__countof_helper(UNALIGNED T (&_Array)[N]))[N];
#define _countof(_Array) (sizeof(*__countof_helper(_Array)) + 0)
}
#endif
#endif

C언어를 사용할 때와 C++를 사용할 때의 구현이 다르게 되어있다.
C언어에서 사용되는 방식은 대부분의 사람들이 잘 알고 있는 방식일 것이며, C++에서는 템플릿을 이용해서 배열의 개수를 구하고 있다.

여기서 궁금해해야 할 점은
  1. 템플릿을 통해 어떻게 배열의 원소의 개수를 구할 수 있는가.
  2. 도대체 왜 C와 C++을 전처리기를 사용해서까지 따로 구현했을까. 그냥 C 구현 하나만 쓰지.

1번 질문의 답은 아래 블로그에 잘 설명이 되어있다.
C++ 템플릿으로 배열의 원소 개수를 구하는 방법

2번 질문의 답은 C 구현 방식에 약간의 문제가 있기 때문이다.
배열이 아니라 포인터일 경우에 C 방식은 제대로 개수를 구해주지 못한다.
그럼 템플릿 방식은 제대로 구해주냐 하면 물론 그럴수는 없다. 그래도 컴파일 에러를 내주기 때문에 좀 더 낫다. C 방식은 컴파일이 잘 되어버리며 잘못된 결과를 돌려준다.
아래 코드를 한번 보자. 설명을 쉽게 하기 위해 매크로 이름을 바꿨다.
#define _countof_c(_Array) (sizeof(_Array) / sizeof(_Array[0]))

template <typename T, size_t N>
char (*__countof_helper(T (&_Array)[N]))[N];
#define _countof_cpp(_Array) (sizeof(*__countof_helper(_Array)) + 0)

void c_version(int* p) // int ar[] 과 같은 형식으로 넘어올 때도 마찬가지이다.
{
    printf("%d\n", _countof_c(p));
}

void cpp_version(int* p)
{
    printf("%d\n", _countof_cpp(p));
}

int _tmain(int argc, _TCHAR* argv[])
{
    int a[100];
    printf("%d\n", _countof_c(a)); // 100 올바른 결과
    printf("%d\n", _countof_cpp(a)); // 100 올바른 결과

    c_version(a); // 1 틀린 결과를 내어줘버렸다. 다행히 경고(C6384)는 발생한다.
    cpp_version(a); // cpp_version함수는 컴파일이 안된다.

    return 0;
}

여기서 배워야 할 점은,
  • C 방식 매크로를 쓸 때 언제 문제가 되는지를 알고 있어야 한다.
  • C++를 사용함에도 불구하고 해당 매크로를 C방식으로 직접 만들어서 사용하는 경우를 보았다. 그냥 _countof를 사용한다.
  • 마지막으로, 컴파일러 경고가 발생하면 무시하지 않는다.
또한, 사실 이는 _countof 의 MSDN 페이지에도 잘 나와있는 내용이다. MSDN을 읽을 때는 꼭 개요부터 See Also까지 다 읽는 습관을 들여야 한다. 특히 Remarks 섹션은 유의해야 할 사항들이나 사용자 쪽에서 궁금해 할만한 내부 구현 방식들을 다루어주므로 항상 눈을 부릅뜨고 읽어봐야한다.
저작자 표시 비영리 동일 조건 변경 허락
신고

'Programming' 카테고리의 다른 글

윈도우 드라이버를 만들 때 알아야 할 기초적인 내용들  (2) 2011.05.23
Duff's Device  (0) 2011.04.07
_countof 매크로  (0) 2011.03.15
FIELD_OFFSET 매크로  (1) 2011.03.01
PAGED_CODE 매크로  (5) 2011.02.27
디렉터리의 읽기 전용 속성  (4) 2011.02.20

submit

FIELD_OFFSET 매크로

2011.03.01 08:00 | Programming

typedef struct tagST
{
  CHAR a;
  CHAR b;
  INT* c;
  INT64 d;
  INT cbName;
  WCHAR name[1];
} ST;
위와 같은 구조체가 있다고 하자. 네번째에 있는 멤버 d의 오프셋을 어떻게 구할지 한 번 생각해보라.

INT offset = sizeof(CHAR) + sizeof(CHAR) + sizeof(INT*);
위와 같이 코드를 작성 했다면 틀렸다. 컴파일러의 구조체 멤버 정렬값에 따라서 결과가 다르게 나올 수도 있기 때문이다.

이런 구조체의 특정 멤버에 대한 오프셋을 구해주는 서비스 매크로가 바로 FIELD_OFFSET이다.
FILED_OFFSET 매크로는 다음과 같이 생겼다.
#define FIELD_OFFSET(type, field) ((LONG)(LONG_PTR)&(((type *)0)->field))
0을 type*으로 형변환 해서 field를 참조하는 부분이 포인트인데 문법 자체에는 너무 신경쓰지 않는게 좋을 것이다.

이 매크로는 아래 처럼 사용한다.
// 첫번째 인자에 구조체 이름을 넣고 두번째 인자로 멤버 이름을 넣는다.
INT offset = FIELD_OFFSET(ST, d);
이 코드는 컴파일러의 정렬 크기나, 64비트 환경들에 상관없이 모두 제대로된 결과를 반환한다.

위 구조체를 다시 한번 보자. 마지막 멤버 name[1]이 조금 이상하게 보일 것이다.
이는 C언어에서 구조체 내에 가변길이의 데이터 멤버를 포함시킬 때 메모리를 두 덩어리로 할당하지 않고 한 덩어리로 할당하기 위해 흔히 쓰이는 기법이다. 성능상의 이점이 있기 때문에 커널 레벨 드라이버등 로우레벨로 내려갈수록 많이 쓰이지만 하이레벨 계층으로 올라오면 거의 쓰이지 않는다. 사용하기 불편하기 때문이다.
이런 구조체에는 2가지 법칙이 있는데 첫번째는 언제나 그 가변 길이 멤버가 맨 아래에 위치하고 있다는 것이며 2번째는 그 가변길이 변수의 크기를 나타내는 추가적인 변수가 꼭 존재한다는 것이다. 여기서는 cbName이다.

이제 이 name이라는 변수에 L"some string"이라는 문자열을 복사해보려고 한다. 이 구조체에 메모리를 어떻게 할당하고 값을 채워넣어야 할까.

typedef struct tagST
{
    CHAR a;
    CHAR b;
    INT* c;
    INT64 d;
    INT cbName;
    WCHAR name[1]; // 널 종료 문자열이 아니다
} ST;

int _tmain(int argc, _TCHAR* argv[])
{
    CONST WCHAR* psz = L"some string";
    INT cch = wcslen(psz);
    INT cb = cch * sizeof(WCHAR);
    
    // 첫번째 방법
    {     
        // 구조체 전체의 크기에 가변 문자열의 크기를 더해서 메모리를 할당한다.
        // 구조체에 name[1]이 이미 포함되어 있으므로 WCHAR 1개 만큼을 다시 빼주어야 한다.
        ST* p = (ST*)malloc(sizeof(ST) + cb - sizeof(WCHAR)));
        memcpy(p->name, psz, cb);
        p->cbName = cb;    
    }
    // 두번째 방법  
    {       
        // 처음부터 name의 오프셋까지만 얻어낸 뒤 cb를 더해주면 조금 더 간단하다.
        // 첫번째 방법처럼 중복된 WCHAR 만큼을 다시 빼줄 필요가 없다.
        ST* p = (ST*)malloc(FIELD_OFFSET(ST, name) + cb);
        memcpy(p->name, psz, cb);
        p->cbName = cb;
    }
    // 세번째 방법
    {
        // FIELD_OFFSET에 항상 멤버의 이름만 쓸수 있는 것은 아니다.
        // 아래처럼 배열의 인덱스에 변수를 명시하는 것도 가능하다.
        // 이 때 cb가 아니라 cch를 넣고 있는 것에 유의해야 한다.
        ST* p = (ST*)malloc(FIELD_OFFSET(ST, name[cch]));
        memcpy(p->name, psz, cb);
        p->cbName = cb;
    }

    return 0;
}

FIELD_OFFSET을 모르고 있으면 첫번째 방법처럼 조금 불편하게 코딩해야 한다는 것을 보여주기 위한 예제였다.

FIELD_OFFSET은 또한 CONTAINING_RECORD 매크로를 만들기 위해서도 쓰인다.
CONTAINING_RECORD는 재미있고 또 중요한 매크로이지만 이 곳 블로그에 이미 설명이 되어 있기 때문에 따로 쓰지 않겠다.

FIELD_OFFSET의 ANSI C 버전은 offsetof이며 stddef.h에 정의되어 있다. CONTAINING_RECORD는 container_of와 같다.
리눅스를 다루는 사람들은 offsetof 매크로를 많이 사용하는 것 같지만 나는 윈도 매크로가 더 익숙해서, 코드를 다른 플랫폼으로 이식할 필요가 없다면 offsetof 보다 FIELD_OFFSET을 사용하는 것을 더 선호한다.
양쪽의 구현은 똑같다.

저작자 표시 비영리 동일 조건 변경 허락
신고

'Programming' 카테고리의 다른 글

Duff's Device  (0) 2011.04.07
_countof 매크로  (0) 2011.03.15
FIELD_OFFSET 매크로  (1) 2011.03.01
PAGED_CODE 매크로  (5) 2011.02.27
디렉터리의 읽기 전용 속성  (4) 2011.02.20
알쏭달쏭한 typedef  (9) 2011.01.04
  1. 오곡 at 2013.08.07 18:14 신고 [edit/del]

    잘배우고 갑니다~!

    Reply

submit
Jeffrey RichterWindows via C/C++ 예제 코드에는 공통 헤더파일이 있는데, 이 곳을 살펴보면 유용하게 사용할 수 있을만한 팁들이 많이 있다.

그 중 가장 쉽고 편하게 쓸 수 있는 기능 하나를 소개하고자 한다.

코드를 작성하다가, '이 부분은 나중에 고쳐야지' 하고 주석으로 마킹해 놓은 뒤에 나중에 잊어버리고 그대로 릴리즈했던 경험이 있는 사람이라면 이 매크로를 아주 좋아하게 될 것이다.

그의 예제코드 중 CmnHdr.h 라는 파일을 보면 다음과 같은 코드가 있다.

//// Pragma message helper macro ////
/* 
When the compiler sees a line like this:
   #pragma chMSG(Fix this later)

it outputs a line like this:

  c:\CD\CmnHdr.h(82):Fix this later

You can easily jump directly to this line and 
 examine the surrounding code.
*/

#define chSTR2(x) #x
#define chSTR(x)  chSTR2(x)
#define chMSG(desc) message(__FILE__ "(" chSTR(__LINE__) "):" #desc)


주석에 잘 쓰여 있듯이 Pragma 지시어를 이용해서 코드 어떤 부분에,
#pragma chMSG(나중에 고칠 것)
int c = a + b;
이런 식으로 주석 대신 적어두는 것이다.

이제 빌드를 하게 되면, Output 창에 이 메세지가 나타나게 되므로 실수를 줄일수 있다.
또한 에러나 경고 메세지와 같이, 더블 클릭 하게되면 해당라인으로 바로 이동하게 된다. 이것은 pragma message의 기능이 아니라, Jeffry가 매크로에 파일과 라인수를 Output창이 알아볼 수 있는 형태로 잘 정의해두었기 때문이다.

하지만 그럼에도 불구하고, 빌드 되는 동안 다른 경고들이 화면 가득 나와서 아예 경고 메세지를 쳐다보지 않는 사람들에게는 혜택이 없다.

이 지시어는 아주 유용하긴 하지만 나는 조금 더 쓰기 편하도록 다음과 같이 매크로로 고쳐서 사용하고 있다.
 
#define chSTR2(x) #x
#define chSTR(x)  chSTR2(x)

#define chMSG(desc) message(__FILE__ "(" chSTR(__LINE__) "): --------" #desc "--------")
#define chFixLater message(__FILE__ "(" chSTR(__LINE__) "): --------Fix this later--------")

#define FixLater \
    do { \
    __pragma(chFixLater) \
    __pragma (warning(push)) \
    __pragma (warning(disable:4127)) \
    } while(0) \
    __pragma (warning(pop))

#define MSG(desc) \
    do { \
    __pragma(chMSG(desc)) \
    __pragma (warning(push)) \
    __pragma (warning(disable:4127)) \
    } while(0) \
    __pragma (warning(pop))



우선은 코드 중간 중간에 #pragma를 쑤셔넣는 것이 보기가 싫었는데, 이 pragma를 매크로 안으로 넣어버렸다. MSVC에는 __pragma라는 키워드를 사용할 수 있는데, 매크로 안에서 pragma 지시어을 사용하기 위해 고안되었다.
만일 예전에 매크로를 만들다가 매크로 안에 #pragma 지시어까지 넣을 수 없을까 고민했던 적이 있던 사람에게는 아주 좋은 소식일 것이다.

또 하나는 코드 맨 끝에 세미콜론을 붙여야 컴파일 되도록 강제하였다. #pragma 지시어는 C문법이 아니므로 세미콜론을 써줄 필요가 없는데, 코드 중간 중간에 들어갈 매크로인만큼 세미콜론이 없으면 미관상에도 안좋고, 복사해서 붙여넣기 등을 할 때 들여쓰기가 깨져버리는 문제가 있다.

그래서 보통 매크로를 만들 때는 세미콜론을 꼭 붙여야 정상적으로 컴파일 되도록 작성하는 것이 좋은데, 위 매크로에서는 do while 얍삽이를 통해서 세미콜론을 강제하고 있다.
저 얍삽이는 Ace 프레임워크의 ACE_DEBUG 매크로를 살펴보다가 알게 된 것인데, 세상에는 참 얍삽하게 머리 좋은 사람들이 많구나 하는 생각을 했다.
농담이다.

do while 얍삽이를 쓰게되면, while(0) 때문에 경고가 발생하는데, 이 역시 __pragma로 감싸버려서 없앨 수 있다.

마지막으로 앞뒤로 ----를 붙여서 좀 더 눈에 띄기 쉽도록 하였다.

이제 다음과 같이 사용할 수 있다.

#pragma chMSG(블라블라블라)
int main()
{
    FixLater;
    int a;

    MSG(나중에 고칠 것);
    return 0;
}

신고
  1. Favicon of http://eslife.tistory.com BlogIcon eslife at 2010.01.14 23:28 신고 [edit/del]

    재호님 글 잘 봤습니다.
    저도 가끔씩 이용하는 방식인데.. 재호님처럼 매크로와 얍샵이 방법까지 활용할 생각은 못해 봤습니다. 좋은 방법 배우고 갑니다 ^^

    Reply
  2. Favicon of http://www.voiceportal.co.kr BlogIcon 김태정 at 2010.01.21 00:43 신고 [edit/del]

    오호~ 오랜만에 글?? ㅎㅎ

    Reply

submit