본문 바로가기

프로그래밍/C&C++

Metaprogramming

반응형

혹시 메타인지라는 단어를 들어 보셨나요?
메타인지는 자신에 대해서 자각하는 정도를 뜻하는 표현이에요.
이와 비슷하게 메타프로그래밍은 프로그램 자신을 변형시키는 프로그래밍을 뜻해요.
 
C++ 같은 컴파일 언어에서는 macro를 사용해 자동으로 코드를 생성 가능하고,
Java 같은 런타임 언어에서는 reflection을 통해 코드를 실행 중에 분석 및 분기할 수 있어요.
다만 macro/reflection은(는) 각각 컴파일/처리 시간을 증가시키기에 너무 빈번한 사용은 권장하지 않아요.
 
reflection은 주로 특정한 이름 규칙이 있는 클래스를 동적으로 호출할 때, 메서드/필드의 접근자를 무시하고 호출할 때 (anti-pattern), 메서드/필드의 정보(e.g. 이름, 접근자)를 동적으로 분석 및 분기할 때, 등의 경우에 사용해요.
 
그렇다면 컴파일 언어에서의 메타프로그래밍은 어떤 경우에 사용하는 걸까요?



저수준 언어에서의 열거체(enum)는 구조체(struct)가 아니라 문법 설탕에 불과한 정수라는 사실을 알고 계셨나요?


따라서 고수준 언어에서 values() 등의 메서드로 쉽게 순회할 수 있는 것과 달리,

대부분의 저수준 언어에서는 열거체를 순회하려면 시작과 끝을 반드시 알고 있어야 해요.

enum MY_ENUM : size_t
{
	A,
	B,
	C,
	LAST,
};

int main()
{
	// range based for loop
	for (const auto entry : MY_ENUM) // 'MY_ENUM' does not refer to a value
	{
		// 우으...
	}
	// traditional for loop
	for (size_t i {0}; i != MY_ENUM::LAST; ++i)
	{
		const auto entry {static_cast<MY_ENUM>(i)};
	}
}

 
C/C++의 여러 불편한 점을 대폭 개선한 Rust에서도 열거체는 직접 순회를 해야 하는 번거로움이 있어요.
그리고 각 열거체에 대응하는 값을 지정하고 싶을 경우에는 수고로움이 배로 늘어나고 오타의 위험도 커져요.

enum MY_ENUM : size_t
{
	A,
	B,
	C,
	LAST,
};

const char* get_name(MY_ENUM value) // or std::string
{
	switch (vaue)
	{
		case A: { return "A"; }
		case B: { return "B"; }
		case C: { return "C"; }
	}
	return nullptr;
}

 
macro를 사용하면 이러한 수작업 과정을 생략해서 보다 유지보수가 수월한 코드를 작성 가능해요.

#define MACRO_FOR_MY_ENUM($) \
$(A, "A")                    \
$(B, "B")                    \
$(C, "C")                    \

struct MY_ENUM
{
	#define macro(name, value) name,
	enum Kind : size_t
	{
		MACRO_FOR_MY_ENUM(macro)
	};
	#undef macro

	#define macro(name, value) value,
	static const auto& values()
	{
		static const char* impl[]
		{
			MACRO_FOR_MY_ENUM(macro)
		};
		return impl;
	};
	#undef macro

	#define macro(name, value) Kind::name,
	static const auto& entries()
	{
		static const Kind impl[]
		{
			MACRO_FOR_MY_ENUM(macro)
		};
		return impl;
	};
	#undef macro
};

 

추가로, Rust는 AST를 기반으로 보다 강력한 매크로 기능을 지원해요.

#![allow(non_camel_case_types)]

macro_rules! entries
{
	(
		$visibility:vis $name:ident -> $type:ty
		{
			$($variant:ident: $value:expr,)+
		}
	)
	=>
	{
		#[derive(Clone, Copy)]
		$visibility enum $name
		{
			$($variant),+
		}

		impl $name
		{
			// associated constant values
			const VALUES: &'static [$type] = &[$($value),+];
			// the enum variants as an array
			const VARIANTS: &'static [Self] = &[$(Self::$variant),+];

			pub fn value(&self) -> &'static $type
			{
				&Self::VALUES[*self as usize]
			}
			
			pub fn variants() -> &'static [Self]
			{
				&Self::VARIANTS
			}
		}
	};
}

entries!(pub MY_ENUM -> &str
{
	A: "A",
	B: "B",
	C: "C",
});

이 밖에도 매크로를 사용해서 반복되는 기능을 함수로 추출하면

호출로 인한 오버헤드는 최소화 하면서 소스코드의 중복은 방지할 수 있어요.

다만 이 경우에는 컴파일 결과물의 크기가 커진다는 단점이 있기에 상황에 따라서 독이 될 수 있어요.

 
반응형

'프로그래밍 > C&C++' 카테고리의 다른 글

the rule of 0/3/5  (1) 2025.03.18
String & SSO  (3) 2025.02.17
Union & Bit-field  (0) 2025.02.17
Struct Padding  (8) 2025.02.13
Unicode  (1) 2025.01.19