본문 바로가기
C & C++

[C/C++] C 언어에서 상속과 다형성 구현하기

by yhames 2024. 7. 29.
728x90


Ray Tracing in One Weekend레이 트레이싱에 대한 기초적인 개념을 설명하고 있습니다. 하지만 예제가 C++로 작성되어 있기 때문에 miniRT 과제를 위해서는 C++ 코드를 C 스타일로 변경해야합니다. 대부분의 내용은 그대로 사용할 수 있지만, 다수의 오브젝트를 처리할 때 상속과 다형성을 활용해서 Hittable와 Hittable_list 클래스를 구현하는 부분은 C에서 구현하기 까다롭습니다. 이 글에서는 Hittable와 Hittable_list 클래스를 위해 C언어에서 상속과 다형성을 활용했던 내용을 공유하려고 합니다.
 

상속과 다형성

상속이란 객체 간의 계층 구조를 형성하는 것을 의미합니다. 상위 계층의 객체를 하위 계층에서 상속받으면, 따로 속성과 행동을 정의하지 않아도 상위 계층에서 정의한 내용을 그대로 사용하는 방식입니다. 예를 들어 Sphere 클래스 Hittable 클래스를 상속받는다면, Hittable 클래스의 hit 함수나 shape 변수를 선언하지 않아도 사용할 수 있습니다.
 
다형성이란 하나의 객체가 여러 가지 타입을 가질 수 있는 것을 의미합니다. C++이나 Java에서는 부모 클래스 타입의 포인터(참조 변수)로 자식 클래스 타입의 인스턴스를 참조할 수 있도록 하여 구현하고 있습니다. 다형성을 활용하면 특정 클래스를 상속받는 서로 다른 객체를 마치 동일 클래스인 것처럼 관리하거나, 변경사항이 발생하는 경우 마치 부품을 갈아끼우는 것처럼 쉽게 유지보수를 할 수 있다는 장점이 있습니다.
 

C언어에서의 상속과 다형성

 
이번 과제에서 구현해야하는 Ray Tracing은 여러 도형을 Hittable 클래스 타입의 리스트로 저장하고, 해당 리스트를 순회하면서 hit() 함수를 호출해야합니다. 모든 도형이 Hittable 클래스를 상속받고, 다형성을 활용하여 순회하면서 hit() 함수를 호출하는 부분을 C언어에서도 활용하면 과제를 진행하는데 도움이 될 것같았고, 이전에 전문가를 위한 C라는 책을 통해 C언어에서 OOP를 구현하는 방법이 있다는 것을 알고 있었기 떄문에 이번 기회에 한번 활용해봐야겠다고 생각했습니다.
 

C언어에서 상속 활용하기


C언어에서 상속을 구현하려면, 자식 구조체의 상단에 부모 구조체 변수를 선언해야합니다. C언어에서는 형변환시 타입체크을 하지 않기 때문에 다른 구조체로 형변환이 가능하고, 자식 구조체의 시작 주소와 부모 구조체의 시작 주소가 동일하기 때문에 잘못된 주소로 접근하는 충돌이 발생하지 않습니다.

enum e_shape
{
	SPHERE,
	PLANE,
	CYLINDER
};

typedef struct s_hittable
{
	t_bool		(*hit)(struct s_hittable *obj, t_ray const *ray, float ray_tmin, float ray_tmax, t_hit_record *hit_record);
	enum e_shape	shape;
}	t_hittable;


먼저 부모 구조체를 보시면 C++의 클래스 처럼 멤버함수를 구현하기 위해 부모 구조체에 함수 포인터를 선언했습니다. 이렇게 선언하면 자식 구조체 변수를 초기화할때 자식 구조체의 함수를 할당하여 사용할 수 있습니다. 또한 다운캐스팅을 안전하게 하기 위해 이 변수가 어떤 도형인지를 나타내는 shape 변수를 enum 타입으로 선언했습니다.

typedef struct s_sphere
{
	t_hittable	parent;	// 부모 객체(구조체)를 첫 번째 변수로 선언
	t_point3	center;
	float		radius;
	t_color		color;
}	t_sphere;

t_sphere	*init_sphere(t_point3 center, float radius, t_color color);
t_bool		hit_sphere(t_hittable *obj, t_ray const *ray, float ray_tmin, float ray_tmax, t_hit_record *rec);

 
다음은 상속받는 구조체입니다. 부모 구조체는 반드시 가장 상단에 위치해야 부모 구조체로 형변환시 시작주소가 동일하기 때문에 주소 충돌이 발생하지 않습니다. 추가로 해당 구조체의 함수에서 사용할 변수를 선언했습니다.
 

C언어에서 다형성 활용하기

 
위에서 자식 구조체가 부모 구조체를 상속받았다면 부모와 자식 간에 형변환이 자유롭기 때문에 여러가지 도형을 hittable 구조체 리스트로 사용할 수 있습니다. 또한 함수포인터를 사용해서 hit() 함수를 선언했기 때문에 자식 구조체에서 초기화할 때 자식 구조체의 멤버 함수를 할당하면 부모 구조체 타입으로 함수를 호출해도 자식 구조체의 멤버함수를 사용할 수 있습니다.
 

t_sphere    *init_sphere(t_point3 center, float radius, t_color color)
{
	t_sphere *sphere;

	sphere = (t_sphere *)malloc(sizeof(t_sphere));
	if (!sphere)
		return (NULL);
	sphere->parent.shape = SPHERE;
	sphere->parent.hit = hit_sphere;	// 자식 객체의 함수 할당
	sphere->center = center;
	sphere->radius = radius;
	sphere->color = color;
	return (sphere);
}


먼저 자식 구조체를 초기화하는 함수입니다. 부모 구조체 타입으로 선언한 parent 변수의 shape과 hit() 함수 포인터에 자식 구조체에서 사용하는 SPHERE와 hit_sphere() 함수를 각각 할당했습니다. 또한 자식 구조체의 멤버함수에서만 사용하는 추가적인 필드에 대해서 초기화를 했습니다.
 

t_bool	hit_sphere(t_hittable *obj, t_ray const *ray, float t_min, float t_max, t_hit_record *rec)
{
	t_sphere	*sphere;
	float		root;
	t_vec3		outward_normal;

	sphere = (t_sphere *)obj;
	if (!is_collided(sphere, ray, t_min, t_max, &root))
		return (FALSE);
	rec->t = root;
	rec->p = point_at(ray, rec->t);
	rec->normal = vec3_div(vec3_sub(rec->p, sphere->center), sphere->radius);
	outward_normal = vec3_div(vec3_sub(rec->p, sphere->center), sphere->radius);
	set_face_normal(rec, ray, outward_normal);
	return (TRUE);
}

 
자식 구조체에서 사용하는 hit_sphere() 함수 입니다. t_hittable 타입 포인터를 매개변수로 받아서 t_sphere 타입 포인터로 형변환을 했습니다. 위에서 말씀드린대로, C언어에는 형변환시 타입 체크를 하지 않습니다. 또한 t_sphere 포인터의 시작 주소에 t_hittable 객체가 있기때문에 t_hittable 포인터를 t_sphere 포인터로 형변환을 해도 잘못된 주소를 참조하는 일이 발생하지 않습니다.
 

t_hittable_list *init_world(t_hittable_list *world, t_camera *camera, char *filename)
{
	t_hittable	*shape;

	// ...
	shape = (t_hittable *)init_sphere((t_vec3){0, 0, -1}, 0.5, (t_color){1, 0, 0});	// 자식 → 부모 형변환(Upcasting) 가능
	add_hittable_list(world, shape);
	// ...
}

 
t_hittable_list의 t_hittable 타입 리스트를 초기화하는 함수입니다. t_sphere 타입 포인터를 t_hittable 타입 포인터로 형변환하여 리스트에 할당했습니다.
 

t_bool	hit_shapes(t_hittable_list *list, t_ray const *ray, float t_min, float t_max, t_hit_record *rec)
{
	t_bool		hit_anything;
	t_hit_record	tmp;
	int		i;
	int		closest_so_far;

	closest_so_far = t_max;
	hit_anything = FALSE;
	i = 0;
	while (i < list->size)
	{
		if (list->objects[i]->hit(list->objects[i], ray, t_min, closest_so_far, &tmp)) {	// 함수 포인터를 통해 자식 구조체의 함수 호출
			hit_anything = TRUE;
			closest_so_far = tmp.t;
			*rec = tmp;
		}
		i++;
	}
	return (hit_anything);
}

 
이제 저희가 원했던 방식대로 어떤 도형이 추가되어도 t_hittable 타입에서 hit() 함수를 호출하여 ray의 충돌 판정을 할 수 있게 되었습니다. hit() 함수를 호출하면 도형을 초기화할 때 할당했던 hit_sphere를 호출하게 됩니다.
 

참고자료

 
1-1. 객체지향 C언어? (클래스, 다형성)

1-1. 객체지향 C언어? (클래스, 다형성)

객체지향이란? C언어의 객체지향이라는 주제로 이야기를 하려면, 우선 "객체지향"이란 무엇인지부터 짚고 넘어가야 한다. 나는 개인적으로 "클린 아키텍쳐"에서 말하는 객체지향 개념을 선호한

www.kernelpanic.kr

1-2. 객체지향 C언어? (상속, 캡슐화)

1-2. 객체지향 C언어? (상속, 캡슐화)

www.kernelpanic.kr

 

반응형