Cover image of latest post

System Programming

Programming

Right-to-Left Rule과 typedef

System Programming - February 8, 2023

C Right-Left Rule (Rick Ord's CSE 30 - UC San Diego) 를 참고하였습니다.

#

Right to Left rule

C는 type이 굉장히 복잡한 언어이다. 이는 포인터(pointer), 배열(array), 함수(function)의 합작의 결과인데, 이들이 섞이는 순간 미치게 된다. 예를들어, 다음과 같은 선언(Declaration)을 쉽게 해석할 수 있겠는가?

int *(*(*fp1)(int))[10];

쉽지 않다. 저것이 함수 포인터인지, 배열에 대한 포인터인지, 뭔가 많이 섞였는데 어느 순서로 섞였는지 쉽게 파악이 안된다. 그래서, 이를 복호화(decipher) 하는 과정이 필요한데, 이를 Right to Left(이하 RL) rule 이라고 한다.

##

준비

앞으로는 영어를 사용하자(??). RL rule은 영어권 사용자를 위해 만들어진 방법으로, 어순이 반대인 한국어로는 이해하기 어렵다. 즉, 한국어 사용자는 다음과 같은 과정을 거쳐야 한다.

암호같은 C 변수 선언문 ----> RL을 사용하여 영어로 복호화 ----> 그 영문장을 한국어로 번역!

영문장을 한국어로 번역하는 과정은 그리 어렵지 않다. 그러면, RL rule에 대해 알아보자.

##

RL rule

변수의 선언에서 다음 기호를 만나면, 아래에 표시된 대로 읽는다.

*            "pointer to"                 번역하면 "~의 포인터"
[]           "array of"                   번역하면 "~의 배열"
()           "function returning"         번역하면 "~를 반환하는 함수"

일단은 영어로 읽기로 하자. 그럼, 어떻게 기호를 만날까?
다음과 같은 순서를 따른다.

  1. 식별자를 찾는다.

    식별자(identifier)는 변수의 이름으로, 유일하다. 여기가 출발점이다. 그러면, 문장을 다음과 같이 시작하면 된다.

    식별자 is ...
    
  2. 오른쪽으로 이동!
    • 식별자로부터 오른쪽으로 이동할 수 있을 때까지 오른쪽으로 이동하면서 기호를 읽는다. 즉, 만일 식별자 오른쪽에 []가 있다면 식별자 is array of ... 으로 읽으면 된다.
    • 만일, (를 만난다면, 그것은 ()의 일부분일 것이다. 즉, () 전체를 식별자 is function returning ... 이라고 읽고 넘어가면 된다. - 하지만, ( 를 만나지 않고 ) 만 만난다면, 그것은 함수의 괄호가 아닌 다른 괄호의 닫는 괄호이다. 즉, 오른쪽으로 이동하는 것을 멈춰야 한다.
  3. 왼쪽으로 이동!
    • 오른쪽으로 가기를 멈췄다면, 식별자로부터 왼쪽으로 이동할 수 있을 때까지 왼쪽으로 이동하면서 기호를 읽는다. 즉, 왼쪽에 *가 있다면 식별자 is pointer to ... 으로 읽으면 된다.
    • ( 는 여는 괄호이므로, 만난다면 멈춘다.
    • 만일 멈춘 후, 오른쪽으로 갈 수 있다면 다시 오른쪽으로 간다 (오른쪽 가다가 멈췄던 곳 부터).
##

예시

간단하게, 다음을 읽어보자.

int *p[];

(배열 포인터인지 포인터 배열인지 헷갈린다면, 꼭 아래의 해설을 천천히 따라가보자!)

  1. 식별자 찾기

    식별자는 p이다.

    int *p[];
         ^
    

    즉, “p is

  2. 오른쪽으로 이동!

    1. 오른쪽으로 이동한다. []가 보인다.

      int *p[];
            ^^
      

      즉, “p is array of

    2. 더 이상 오른쪽으로 갈 수 없다. (세미콜론이 있기 때문) 따라서 멈추고, 다음 단계를 따른다.

  3. 왼쪽으로 이동!

    1. 왼쪽으로 이동한다. *가 보인다.

      int *p[];
          ^
      

      즉, “p is array of pointer to

    2. 왼쪽으로 이동한다. int가 보인다.

      int *p[];
      ^^^
      

      즉, “p is array of pointer to int

    3. 더 이상 왼쪽으로 이동할 수 없고, 오른쪽으로도 갈 수 없다. 종료.

즉, 답은 p is array of pointer to int 가 된다. 쉽지 않은가?

조금 더 복잡한 예제를 풀어보자.

int *(*func())();
  1. 식별자 찾기

    식별자는 func 이다.

    int *(*func())();
           ^^^^
    

    즉, “func is

  2. 오른쪽으로 이동!

    1. 한 번 이동하면 ()가 나온다.

      int *(*func())();
                 ^^
      

      즉, “func is function returning

    2. 오른쪽이 닫는 괄호 ) 로 막혀있다. 즉, 왼쪽으로 이동한다.

  3. 왼쪽으로 이동!

    1. 식별자 왼쪽에 *가 있다.

      int *(*func())();
            ^
      

      즉, “func is function returning pointer to

    2. 왼쪽이 여는 괄호 ( 로 막혀있다. 따라서 다시 오른쪽으로 간다.

  4. 오른쪽으로 이동!

    1. 앞서 멈춘 쪽에서 오른쪽으로 가면 ()가 나온다.

      int *(*func())();
                    ^^
      

      즉, “func is function returning pointer to function returning

    2. ;에 의해 막히므로 다시 왼쪽으로 간다.

  5. 왼쪽으로 이동!

    1. 앞서 멈춘 쪽에서 왼쪽으로 가면 *가 나온다.

      int *(*func())();
          ^
      

      즉, “func is function returning pointer to function returning pointer to

    2. 한번 더 가면 int가 나온다.

      int *(*func())();
      ^^^
      

      즉, “func is function returning pointer to function returning pointer to int

    3. 더이상 갈 수 없으므로 종료.

따라서 답은 “func is function returning pointer to function returning pointer to int” 이다.
복잡하지만, 충분히 할만한 작업이다.

만일 배열의 크기가 주어진다면, ([3]과 같이) “array (size 3) of ...” 과 같이 해석하면 될 것이고,
함수의 매개변수가 주어진다면, ((char *, int)와 같이) “function expecting (char *, int) returning ...” 으로 해석하면 될 것이다.

##

번역

어렵지 않다. 영어니까 뒤에서부터 해석하면 되는 것이다. 즉,

int *p[];

“p is array of pointer to int” 이므로, “p는 int의 포인터의 배열이다” 라고 말하면 된다.

마찬가지로

int *(*func())();

“func is function returning pointer to function returning pointer to int” 이므로, ”func는 int의 포인터를 반환하는 함수의 포인터를 반환하는 함수” 가 된다.

그렇다면, 맨 처음 나온 이것은?

int *(*(*fp1)(int))[10];

“fp1 is pointer to function expecting (int) returning pointer to array (size 10) of pointer to int”

번역하면, “fp1은 int의 포인터의 (크기가 10인) 배열의 포인터를 반환하는 (int를 매개변수로 가지는) 함수의 포인터”

##

주의!

###

다른 것은 다르다.

일단, 이 해석 방법에서는 포인터, 배열, 함수를 다르게 보고 있다. 즉, 배열 == 포인터나, 함수 == 포인터 이렇게 생각하면 절대로 안된다는 것이다.

일단 RL로 해석한 다음, 그 뒤에 나름의 논리와 주관을 섞어서 같게 봐야 한다.

###

되는 것과 되지 않는 것

다음과 같은 것은 C에서 허용하지 않는다.

  • []()
    • array of function
  • ()()
    • function returning function
  • ()[]
    • function returning array of …
    • 즉, 배열을 반환할 수는 없다는 것.
#

typedef와의 관계

이렇게 이상한 변수의 선언을 읽는 방법을 알아봤다. 이는 사실 typedef를 해석하는 데에도 유용하다.

아마 대다수의 책에서 typedef에 대해 잘못 설명한다. 다음과 같이 말이다.

typedef <원래 타입> <새로운 타입>;

이는 typedef int pid_t; 와 같이 맞는 것 같지만, 사실 그렇지 않다는 것은 다음만 봐도 알 수 있다.

typedef char u24[3];

(?? 이게 뭐임)

이렇게 보면 함수 포인터의 typedef도 이상하다.

typedef void (*sighandler_t)(int);
##

올바른 설명

typedef에 대한 올바른 설명은 다음과 같다.

typedef <타입의 선언>

여기서, 타입의 선언이라는 것은 식별자(identifier)로 새로운 타입이 온다는 것이다.

식별자가 타입이 되었다는 것을 제외하면, 앞서 봤던 변수의 선언과 별 다를 것이 없다. 예를 들어서 다음을 해석해보자.

typedef void (*sighandler_t)(int);

똑같이 RL rule을 적용하면,

“sighandler_t is pointer to function expecting (int) returning void“

대신, 중간에 이것이 타입이라는 것을 알려주는 말을 넣으면 된다.

“sighandler_t is type which is pointer to function expecting (int) returning void“

한국어로 번역하면,

“sighandler_t는 void를 반환하는 (int를 매개변수로 하는) 함수의 포인터를 나타내는 타입이다

라고 할 수 있겠다.

마찬가지로

typedef char u24[3];

은 다음과 같다.

“u24 is type which is array (size 3) of char”

즉, “u24는 char의 (크기가 3인) 배열을 나타내는 타입이다”

###

아니 이런게 어디에 쓰임…

실제로 쓰인다. glibc의 setjmp.h 를 살펴보면 다음과 같은 구절(?)이 있다.

setjmp.h

typedef struct __jmp_buf_tag jmp_buf[1];

즉, nonlocal jump시 필요한 type인 jmp_buf는 사실, “구조체 __jmp_buf_tag의 (크기가 1인) 배열” 이다. 따라서, setjmp시에 인자로 &jmp_buf가 아닌, 그냥 jmp_buf 를 넘겨주어도 되는 것이다.

##

오…

이렇게 보면 typedef, 괜찮지 않은가? RL rule만 완벽히 터득하면 오히려 직관적이라는 느낌까지 든다.

근데, 이렇게 선언과 동일한 문법을 쓸 것이면, typedec로 이름을 지었으면 더 좋았을 것이라는 생각이 든다.

#

또 다른 팁

사실 이 RL rule은 constant pointer 등의 해석에도 도움이 된다. 다음의 예시를 보자.

const int *p;

“p is pointer to const int”

int * const p;

“p is constant pointer to int”

ㅇㅎ이