C++라이브러리 / property

프로퍼티는 C#에 있는 문법입니다. 프로퍼티는 멤버변수에 대한 캡슐화를 지원하면서 외부에서의 접근을 허용하는 기능입니다. 간단히 말해서 Getter와 Setter를 좀 더 고급지게 사용하는 것과 같습니다. 프로퍼티를 사용하는 사용자의 입장에서 보았을 때 프로퍼티에 읽고 쓰는 것은 멤버변수에 직접 읽고 쓰는 것과 같습니다. 하지만 실질적으론 클래스 내부의 메소드를 통해 값을 읽고 쓰는 것입니다. 간단한 C# 코드로 예시를 살펴보겠습니다. 먼저 프로퍼티를 사용하지 않고 Getter/Setter를 이용해서 구현한 코드입니다.

non_property.cs
class MyClass 
{
  private int myVariable;
  public int getMyVariable() { return myVariable; }
  public void setMyVariable(int value) { myVariable = value; }  
}

...

MyClass A;

int a = A.getMyVariable();
A.setMyVariable(a + 2);
C#

아래는 프로퍼티를 사용한 코드입니다.

property.cs
class MyClass 
{
  private int myVariable;
  public int MyVariable {
    get 
    {
      return myVariable;
    }
    set 
    {
      myVariable = value;
    }
  }
}

...

MyClass A;

int a = A.MyVariable;
A.MyVariable = a + 2;
C#

사실상 두 코드는 똑같은 동작을 수행합니다만 프로퍼티가 미관상 더 보기 좋은 코드를 만듭니다. 언뜻보면 프로퍼티가 은닉성을 요구하는 객체지향 프로그래밍에서 멤버변수를 노출하는 것 처럼 보입니다. 하지만 프로퍼티는 들어올 값의 범위를 제한한다거나 로직에 맞지 않는 값을 걸러내는 기능을 가지며 은닉성을 어느정도 보존할 수 있습니다.

그러나 C++에서 이 기능을 사용해보려니 C++표준은 프로퍼티 문법을 가지고 있지 않습니다. 그래서 매크로를 통해 C++에서 프로퍼티를 구현하는 방법을 소개하겠습니다.

property.hpp
#pragma once

#include <type_traits>
#include <stddef.h>
#include <stdint.h>

template <class T>
class __non_copyable_but {
	friend T;

	__non_copyable_but(const __non_copyable_but<T>&) = default;
	__non_copyable_but(__non_copyable_but<T>&&) = default;
	__non_copyable_but& operator=(const __non_copyable_but<T>&) = default;
	__non_copyable_but& operator=(__non_copyable_but<T>&&) = default;
};

#define __safe_offsetof(type, member) ((size_t)(std::addressof(((type*)0)->member)))

#define __PROPERTY_MAKE_NAME(__prop_type, __prop_name) __PROPERTY_##__prop_name

#define __PROPERTY_GET_ADDRESS_OF(__prop_name) (std::intptr_t)(this) - __safe_offsetof(__this_class_type, __prop_name)

#define __PROPERTY_GET_IMPL(__prop_name)                                                                \
			prop_type get() const {                                                                     \
				auto address = __PROPERTY_GET_ADDRESS_OF(__prop_name);                                  \
				return reinterpret_cast<__this_class_type*>(address)->__property_##__prop_name##_get(); \
			}                                                                                           \
			operator prop_type() const { return this->get(); }

#define __PROPERTY_SET_IMPL(__prop_name)                                                              \
			void set(const prop_type& value) {                                                        \
				auto address = __PROPERTY_GET_ADDRESS_OF(__prop_name);                                \
				reinterpret_cast<__this_class_type*>(address)->__property_##__prop_name##_set(value); \
			}                                                                                         \
			void operator=(const prop_type& value) { this->set(value); }

#define __PROPERTY_DEFAULT_IMPL(__prop_type, __prop_name, __get_impl, __set_impl)        \
	};                                                                                   \
	struct __PROPERTY_MAKE_NAME(__prop_type, __prop_name) {                              \
		using prop_type = __prop_type;                                                   \
		using this_type = __PROPERTY_MAKE_NAME(__prop_type, __prop_name);                \
		__PROPERTY_MAKE_NAME(__prop_type, __prop_name)() = default;                      \
		__PROPERTY_MAKE_NAME(__prop_type, __prop_name)(const this_type&) = default;      \
		__PROPERTY_MAKE_NAME(__prop_type, __prop_name)(const prop_type& T) : value(T) {} \
		this_type& operator=(const this_type&) = delete;                                 \
		this_type& operator=(this_type&&) = delete;                                      \
		__get_impl                                                                       \
		__set_impl                                                                       \
	private:                                                                             \
		prop_type value;                                                                 \
	};                                                                                   \
	union {                                                                              \
		__PROPERTY_MAKE_NAME(__prop_type, __prop_name) __prop_name

#define __PROPERTY_DEFAULT_GET_IMPL                    \
	operator prop_type() const { return value; }       \
	const prop_type& get_ref() const { return value; } \
	prop_type& get_ref() { return value; }             \
	prop_type get() const { return value; }

#define __PROPERTY_DEFAULT_SET_IMPL                                 \
	void operator=(const prop_type& value) { this->value = value; } \
	void set(const prop_type& value) { this->value = value; }

#define PROPERTY_INIT(__class_name)                          \
	using __this_class_type = __class_name;                  \
	const auto* __property_get_this() const { return this; } \
	auto* __property_get_this() { return this; }

#define PROPERTY union

#define PROPERTY_GET(__prop_type, __prop_name)                  \
		struct : public __non_copyable_but<__this_class_type> { \
			using prop_type = __prop_type;                      \
			__PROPERTY_GET_IMPL(__prop_name)                    \
		} __prop_name

#define PROPERTY_SET(__prop_type, __prop_name)                  \
		struct : public __non_copyable_but<__this_class_type> { \
			using prop_type = __prop_type;                      \
			__PROPERTY_SET_IMPL(__prop_name)                    \
		} __prop_name

#define PROPERTY_GET_SET(__prop_type, __prop_name)              \
		struct : public __non_copyable_but<__this_class_type> { \
			using prop_type = __prop_type;                      \
			__PROPERTY_GET_IMPL(__prop_name)                    \
			__PROPERTY_SET_IMPL(__prop_name)                    \
		} __prop_name

#define PROPERTY_DEFAULT_GET(__prop_type, __prop_name) \
	__PROPERTY_DEFAULT_IMPL(__prop_type, __prop_name,  \
		__PROPERTY_DEFAULT_GET_IMPL,)

#define PROPERTY_DEFAULT_SET(__prop_type, __prop_name) \
	__PROPERTY_DEFAULT_IMPL(__prop_type, __prop_name,, \
		__PROPERTY_DEFAULT_SET_IMPL)

#define PROPERTY_DEFAULT_GET_SET(__prop_type, __prop_name) \
	__PROPERTY_DEFAULT_IMPL(__prop_type, __prop_name,      \
		__PROPERTY_DEFAULT_GET_IMPL,                       \
		__PROPERTY_DEFAULT_SET_IMPL)

#define GET(__prop_name) \
	decltype(__prop_name)::prop_type __property_##__prop_name##_get() const

#define SET(__prop_name) \
	void __property_##__prop_name##_set(const decltype(__prop_name)::prop_type& value)

#define GET_IMPL(__class_name, __prop_name) \
	decltype(__class_name::__prop_name)::prop_type __class_name::__property_##__prop_name##_get() const

#define SET_IMPL(__class_name, __prop_name) \
	void __class_name::__property_##__prop_name##_set(const decltype(__class_name::__prop_name)::prop_type& value)
C++

C++에서 프로퍼티를 구현하는 방법은 다양합니다만 모든 방법의 장점만을 취하긴 힘들기 때문에 하나의 구현방법을 선택해야 합니다. 간단하게 구현하기 위해선 std::function<>을 사용하는 방법이 있는데 std::function<>자체가 되게 무겁기도 하고 프로퍼티 특성상 Getter나 Setter가 바뀌는 일은 없기 때문에 낭비가 됩니다. 따라서 매크로와 Anonymous Union/Struct, Nested Class를 이용해서 낭비되는 메모리가 거의 없고 컴파일러 최적화에 유리하며 좀 더 C++스러운 방법으로 구현했습니다. 구현된 정확한 원리는 비주얼 스튜디오같은 IDE에서 매크로 펼치기 기능을 이용해서 직접 보시는게 더 이해하기 편할 겁니다.

사용법은 아래와 같습니다.

프로퍼티를 사용하고자 하는 클래스에 PROPERTY_INIT매크로로 프로퍼티를 사용하도록 선언합니다. private: 블록 안에 위치하는 것이 좋습니다. 그리고 PROPERTY매크로로 블록을 만들고 그 안에 프로퍼티를 선언합니다. 사용할 수 있는 프로퍼티는 아래 6개입니다.

  • PROPERTY_GET(type, name)
  • PROPERTY_SET(type, name)
  • PROPERTY_GET_SET(type, name)
  • PROPERTY_DEFAULT_GET(type, name)
  • PROPERTY_DEFAULT_SET(type, name)
  • PROPERTY_DEFAULT_GET_SET(type, name)

사용하고자 하는 프로퍼티를 선언하기 위해서 해당하는 매크로 뒤에 타입과 프로퍼티 이름을 넣습니다. DEFAULT가 붙은 매크로는 자동 구현 프로퍼티입니다. 따로 Getter/Setter를 구현하지 않아도 됩니다. 자동 구현 프로퍼티는 바로 초기화를 하거나 멤버 이니셜라이저에서 초기화를 할 수 있습니다. 그리고 GET/SET매크로로 Getter/Setter에 대한 선언과 정의를 할 수 있습니다. 만약 정의를 하지 않았다면 따로 GET_IMPL/SET_IMPL매크로로 외부에 정의할 수 있습니다.

property_test.cpp
#include <iostream>
#include <string>
#include <util/property.hpp>

using namespace std;

class MyClass {
	PROPERTY_INIT(MyClass);

public:
	MyClass() :
		prop_default_int1(20) // 자동 구현 프로퍼티는 멤버 이니셜라이저로 초기화 가능
	{
		// 일반 프로퍼티는 멤버변수를 생성자에서 초기화하는 식으로 초기화 가능
		int_member    = 1;
		float_member  = 3.14;
		string_member = "hello";
	}

public:
	PROPERTY {
		PROPERTY_DEFAULT_GET(int, prop_default_int0) = 10; // Getter만 가지는 자동 구현 프로퍼티
		PROPERTY_DEFAULT_SET(int, prop_default_int1);      // Setter만 가지는 자동 구현 프로퍼티
		PROPERTY_DEFAULT_GET_SET(int, prop_default_int2);  // Getter/Setter를 가지는 자동 구현 프로퍼티
	};

	PROPERTY {
		PROPERTY_GET(int, prop_int);           // Getter만 가지는 프로퍼티	
		PROPERTY_SET(float, prop_float);       // Setter만 가지는 프로퍼티
		PROPERTY_GET_SET(string, prop_string); // Getter/Setter를 가지는 프로퍼티
	};

private:
	GET(prop_int) {
		return int_member;
	}

	SET(prop_float) {
		float_member = min(max(value, 0.f), 10.f);
	}

	GET(prop_string); // prop_string 프로퍼티의 Getter/Setter에 대한 선언
	SET(prop_string);

private:
	int         int_member;
	float       float_member;
	std::string string_member;
};

GET_IMPL(MyClass, prop_string) {
	cout << "someone got string message!\n";
	return string_member;
}

SET_IMPL(MyClass, prop_string) {
	cout << "someone set string message!\n";
	cout << "from : " << string_member << endl;
	cout << "to   : " << value << endl;
	string_member = value;
}

int main() {
	MyClass inst;

	int int0 = inst.prop_default_int0;
	// int int1 = inst.prop_default_int1; 에러, 쓰기 전용 프로퍼티
	int int2 = inst.prop_default_int2;

	// inst.prop_default_int0 = 100; 에러, 읽기 전용 프로퍼티
	inst.prop_default_int1 = 100;
	inst.prop_default_int2 = 100;

	int int_value = inst.prop_int;
	// inst.prop_int = 100; 에러, 읽기 전용 프로퍼티
	
	// float float_value = inst.prop_float; 에러, 쓰기 전용 프로퍼티
	inst.prop_float = 100.f; 

	string string_value = inst.prop_string;
	inst.prop_string = "Hello World!";
	cout << inst.prop_string.get() << endl;
	cout << ((string)inst.prop_string).front() << endl; // 캐스팅 후 메소드 접근 가능(단, 복사된 인스턴스임)
	
	// 기타 주의 사항
	// 1. cout << inst.prop_string << endl; 에러

	// 2. auto string_value = inst.prop_string; 에러

	// 3. for (char ch : inst.prop_string) 에러
	//        cout << ch;

	return 0;
}
C++

아래는 출력입니다. string 프로퍼티에 읽기 쓰기를 할 때 마다 출력이 됩니다.

주의해야 할 점은 Getter는 형변환 연산자 오버로딩을 통해 구현해놨기 때문에 암묵적 형변환이 되지 않는 상황에선 작동하지 않습니다. 그럴 땐 프로퍼티의 .get() 메서드로 직접 Getter를 호출 할 수 있습니다. 또 auto로 프로퍼티를 받는다면 프로퍼티 클래스 자체를 복사생성하는 것이 되기 때문에 오류가 생깁니다(안전을 위해 프로퍼티가 복사생성이 안되게 해놨기 때문에 오류가 생깁니다). 추가적으로 만약 크기가 큰 클래스를 프로퍼티에 사용하고자 한다면 std::reference_wrapper를 사용하면 됩니다.

답글 남기기

이메일 주소는 공개되지 않습니다. 필수 필드는 *로 표시됩니다