[C][C-lang]container_of와 offsetof로 멤버 변수로 해당 구조체 반환하기
C코드를 보다보면 container_of와 offsetof라는 함수가 정의되어있다.
더 정확히 말하자면... 둘다 함수는 아니고 매크로 함수라는 차이점이 있다.
사실 이는 아주 마법 같은, 다른 언어에서 할 수 없는 미친행동이 가능한데 그 이유는,
멤버 변수로 해당 구조체를 반환하는 짓이 가능하다는 점이다...
그 이유는 container_of와 offsetof라는 때문이다.
먼저 offsetof라는 매크로함수를 보자.
offsetof
특정 구조체에서 해당 변수가 메모리상에서 얼마나 떨어져있는지를 알려준다.
#define offsetof(s,m) (size_t)&(((s *)0)->m)
해당 매크로는 stddef.h에 정의되어있으므로 c의 경우 stddef.h를, c++의 경우 cstddef를 추가해주면된다.
이 녀석은 구조체에서 해당 변수가 메모리상에서 얼마나 떨어졌는지 본다.
이를 이해하려면 C에서 구조체가 메모리상에서 어떻게 할당되야하는지 봐야한다.
typedef struct _tagTest {
int a;
int b;
int c;
} test;
위와 같은 test라는 구조체가 존재 한다고 가정하자.
test t;
그럼 위와같은 변수가 존재한다고 가정하자.
t의 주소는 100이라고 가정했을때,
t.a의 주소는 t의 주소와 같다.
배열과 구조체, 공용체에서는 제일 선두의 녀석과 그 구조체 인스턴스의 주소는 항상 같다.
test t;
printf("%p\n", &t);
printf("**********\n");
printf("%p\n", &(t.a));
printf("%p\n", (char *) &t + offsetof(test, a));
printf("**********\n");
printf("%p\n", &(t.b));
printf("%p\n", (char *) &t + offsetof(test, b));
printf("**********\n");
printf("%p\n", &(t.c));
printf("%p\n", (char *) &t + offsetof(test, c));
해당 코드가 존재한다고 가정하고 결과를 예측해보자.
0x7ffee5ede930
**********
0x7ffee5ede930
0x7ffee5ede930
**********
0x7ffee5ede934
0x7ffee5ede934
**********
0x7ffee5ede938
0x7ffee5ede938
그러면 위처럼 출력이 된다.
즉 t와 t.a는 같은 주소이며 t.b, t.c는 t.a로 부터 각각 int크기만큼 떨어져있다.
해당 컴퓨터에서 int는 4바이트이므로 결국 4바이트 떨어져있게 된다.
이제 container_of을 보도록하자.
container_of
특정 구조체에서 멤버변수의 주소를 가지고 있을 경우 그 본체의 주소를 반환한다.
#define container_of(ptr, type, member) \
((type *) ((char *) (ptr) - offsetof(type, member)))
이 매크로 함수는 보통 정의되어있지 않고 만들어서 써야한다.
linux커널, libuv등 여러 C라이브러리에서는 이 container_of를 정의해서 사용하고 있다.
이 매크로 함수의 의미는 ptr이 특정 구조체에 속해있을경우(멤버변수 라면) 그 구조체를 반환한다는 매우 사기적은 매크로 함수이다.
가령 이런경우에 유용하게 사용할 수 있다.
typedef struct _tagGun {
int bullet;
int since;
} gun;
typedef struct _tagPerson {
int age;
gun _g;
} person;
int main() {
person p = {1, {20, 2002}};
gun *g = &p._g;
printf("%p\n", &p);
printf("**********\n");
printf("%p\n", container_of(g, person, _g));
printf("%p\n", container_of(g, person, _g));
return 0;
}
person은 gun이라는 무기를 멤버변수로 가진다고 가정하자.
여기서 우리는 p의 _g를 gun *g에 연결하였다.
그러니까 우리는 p의 _g의 값은 알고있는 상태라고 가정하자.
printf("%p\n", container_of(g, person, _g));
그렇다면 우리는 이 총의 소유주가 누구인지 container_of를 이용해서 알아낼 수 있다.
첫번째 파라메터로는 타겟 포인터를, 두번째 변수로는 그 타겟 포인터를 가지고 있는 구조체이름을,
세번째로는 해당 구조체에서 타겟포인터와 같은 타입의 멤버변수를 적어줘야한다.
즉 g와 _g는 무조건 같은 타입이어야한다는 의미, 위의 예제의 경우 1번과 3번은 둘다 gun으로 같은 타입이다.
0x7ffee223f930
**********
0x7ffee223f930
0x7ffee223f930
실행해보면 둘이가 동등한걸 확인할 수있다.
이를 어디다 쓰는고?
아주 다양하게 쓰일 수 있으며 아주 사기적인 성능을 자랑하여 아주 많이 사용한다.
매크로함수 + 포인터 캐스팅이 조화되어서 오류가 날 확률이 높으니 조심해서 써야할 것이다.
아주 자주쓰이는 예시를 들어주겠다.
typedef struct _tagNode {
int data;
struct _tagNode *left;
struct _tagNode *right;
} node;
int main() {
node c = {10, NULL, NULL};
node b = {20, &c, NULL};
node a = {30, &b, NULL};
node **c_ptr = &b.left;
node *parent = container_of(c_ptr,node,left);
printf("c : %d\n",*c_ptr);
printf("b : %d\n",parent->left);
printf("**********\n");
printf("c : %d\n",(*c_ptr)->data);
printf("b : %d\n",parent->data);
return 0;
}
보면 알겠지만 c_ptr은 c의 포인터를 가르키고 있다.
사실상 c를 가르킨다고 봐도 무방한데(진짜로 가르킨다는 뜻은 아님)
c만 아는 상태에서 부모를 알아낼 수 있게 된다.
c : -527132384
b : -527132384
**********
c : 10
b : 20
c를 아는 상태에서 부모를 알아낼 수 있다.