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