일 | 월 | 화 | 수 | 목 | 금 | 토 |
---|---|---|---|---|---|---|
1 | 2 | 3 | 4 | |||
5 | 6 | 7 | 8 | 9 | 10 | 11 |
12 | 13 | 14 | 15 | 16 | 17 | 18 |
19 | 20 | 21 | 22 | 23 | 24 | 25 |
26 | 27 | 28 | 29 | 30 | 31 |
Tags
- resource management class
- 스마트 포인터
- exception
- 암시적 변환
- lua
- 영화
- Smart Pointer
- 반복자
- 다형성
- c++
- UE4
- 게임
- 참조자
- 티스토리챌린지
- 비교 함수 객체
- virtual function
- Effective c++
- 상속
- 오블완
- more effective c++
- 언리얼
- operator new
- 메타테이블
- Vector
- 영화 리뷰
- 루아
- reference
- 예외
- effective stl
- implicit conversion
Archives
- Today
- Total
스토리텔링 개발자
[More Effective C++] 30. 프록시 클래스 본문
728x90
항목 30. 프록시(Proxy) 클래스
C++의 n차원 배열 문제
- 변수를 배열의 차원으로 사용할 수가 없다.
int data[10][20]; // 컴파일 성공
void processInput(int dim1, int dim2)
{
int data[dim1][dim2]; // 컴파일 에러!
...
}
void processInput2(int dim1, int dim2)
{
int* data = new int[dim1][dim2]; // 컴파일 에러!
...
}
2차원 배열 클래스 만들기
template<typename T>
class Array2D
{
public:
Array2D(int dim1, int dim2);
...
};
// 이런 식으로 사용할 수 있다.
Array2D<int> data(10, 20);
Array2D<float>* data = new Array2D<float>(10, 20);
void processInput(int dim1, int dim2)
{
Array2D<int> data(dim1, dim2);
...
}
- 하지만 대괄호를 사용하는 인터페이스가 아닌 것이 아쉽다.
- operator[][]를 재정의할 수 있을까?
- 아쉽게도 그런 연산자는 지원하지 않는다.(항목 7 참조)
- 아쉬운대로 괄호를 사용하여 2차원 배열에 접근하도록 만든다.
template<typename T>
class Array2D
{
public:
T& operator()(int index1, int index2);
const T& operator()(int index1, int index2) const;
...
};
cout << data(3, 6);
- 장점
- 구현하기가 쉽고 차원이 늘어나도 원하는 대로 일반화하기 쉽다.
- 단점
- 대괄호가 들어가 있지 않아서 배열 접근보다는 함수 호출처럼 보인다.
- C++ 기본 다차원 배열을 다시 한 번 훑어보자.
int data[10][20];
...
cout << data[3][6]; // 이는 사실 2차원 배열이 아니다.
// data는 10개의 요소를 가진 1차원 배열 안에 20개의 요소 1차원 배열을 넣은 것이다.
// 즉, (data[3])[6] 이라고 할 수 있다.
- 이 논리를 사용해서 operator[]의 오버로딩을 통해서 이를 구현할 수 있을 것이다.
template<typename T>
class Array2D
{
public:
// 이 클래스에 대해서는 사용자가 전혀 알 필요가 없다.
class Array1D
{
public:
T& operator[](int index);
const T& operator[](int index) const;
...
};
Array1D operator[](int index);
const Array1D operator[](int index) const;
...
};
Array2D<float> data(10, 20);
...
cout << data[3][6]; // 성공!!
- Array1D 객체는 Array2D를 사용하는 사용자를 수면 아래에서 보조해주는데
- 이를 프록시 객체(proxy object)라고 하며, 이 클래스를 프록시 클래스(proxy class)라고 한다.
- 대리자(surrogate)라고 하기도 한다.
operator[]의 읽기 / 쓰기를 구분하기
- 프록시 클래스를 사용해서 operator[]의 읽기와 쓰기 동작을 구분해 보자.
- 앞서 참조 카운팅에서 읽기 / 쓰기 버전의 operator[]를 다루었었다.(항목 29 참조)
- 읽기 / 쓰기 가능한 버전의 경우 객체를 새로 생성하고 공유 플래그를 꺼준 후 리턴하여 처리했다.
-
class String { public: const char& operator[](int index) const; // 읽기 전용 char& operator[](int index); // 쓰기 지원 ... };
- 하지만 이 경우, 호출한 객체가 상수이냐를 따지기 때문에 완벽한 해결책이 아니다.
-
String s1, s1; cout << s1[5]; // 읽기 전용을 쓰고 싶지만, s1은 상수가 아니므로 쓰기 버전이 불린다. s2[5] = 'x'; // 마찬가지 s1[3] = s2[8]; // 역시 마찬가지
- 즉, 무조건 쓰기 동작이 일어날 것임을 상정한 구현이었다.
- 읽기 / 쓰기를 구분하는 기본 아이디어
- operator[]의 결과값이 사용되기 전까지 좌항값 / 우항값 연산을 늦추자.( 지연 평가(항목 17 참조) )
- 그리고 이 지연평가를 프록시 클래스가 맡도록 한다.
class String
{
public:
class CharProxy
{
public:
// 프록시 생성
CharProxy(String& str, int index);
// lvalue 연산용
CharProxy& operator=(const CharProxy& rhs);
charProxy& operator=(char c);
// rvalue 연산용
operator char() const; // char로 암시적 변환
private:
String& thsString;
int charIndex;
};
const CharProxy operator[](int index) const;
CharProxy operator[](int index);
...
friend class CharProxy;
private:
RCPtr<StringValue> value;
};
// operator[]는 문자열 자체를 전혀 수정하지 않고 프록시 객체만 리턴한다.
const String::CharProxy String::operator[](int index) const
{
// 상수 프록시 객체를 리턴하지만,
// CharProxy::operator=는 const 멤버 함수가 아니므로
// 상수 프록시에 대해서는 대입 연산을 사용할 수 없다.(보너스 효과!)
// 또한, const_cast를 사용하여 상수성을 해제하고 있지만,
// 리턴값이 상수 프록시이므로 수정될 염려가 없다.
return CharProxy(const_cast<String&>(*this), index);
}
String::CharProxy String::operator[](int index)
{
return CharProxy(*this, index);
}
String::CharProxy::operator char() const
{
return theString.value->data[charIndex];
}
String::CharProxy& String::CharProxy::operator=(const CharProxy& rhs)
{
// 공유 중이라면, 자신만 수정되도록 사본을 만든다.
if(thisString.value->isShared())
thisString.value = new StringValue(theString.value->data);
// 대입하여 string을 변경해준다.
theString.value->data[charIndex] = rhs.theString.value->data[rhs.charIndex];
return *this;
}
String::CharProxy& String::CharProxy::operator=(char c)
{
// 공유 중이라면, 자신만 수정되도록 사본을 만든다.
if(thisString.value->isShared())
thisString.value = new StringValue(theString.value->data);
// 대입하여 string을 변경해준다.
theString.value->data[charIndex] = c;
return *this;
}
String s1, s2;
cout << s1[5]; // CharProxy의 operator char()로 암시적 변환되어 성공
s2[5] = 'x'; // operator=(char)이 사용되어 내부값 변경
s1[3] = s2[8]; // operator=(CharProxy&)이 사용되어 내부값 변경
프록시 클래스의 단점
- 객체가 lvalue로 사용되는 경우는 대입 연산자 외에도 있기 때문에, 프록시 객체가 능사는 아니다.
- 문제 1. 주소 연산자를 사용하는 경우
String s1 = "Hello";
char* p = &s1[1]; // s1[1]은 CharProxy를 리턴하므로 컴파일 에러!
// char*에 CharProxy*를 대입하려 하나, 이런 변환은 정의되어 있지 않다.
- 주소 연산자(address of, &연산자)를 재정의하여 해결하면 되지 않나?
class String
{
public:
class CharProxy
{
public:
...
char* operator&();
const char* operator&() const;
...
};
...
};
const char* String::CharProxy::operator&() const
{
return &(thsString.value->data[charIndex]);
}
// const가 아닌 버전은 수정될 수도 있는 문자열의 포인터를 반환하므로 주의.
// const가 아닌 String::operator[]와 꽤 흡사하다.(항목 29 참조)
char* String::CharProxy::operator&()
{
// 공유 중이면 수정될 수 있기 때문에 사본 생성
if(thsString.value->isShared())
theString.value = new StringValue(theString.value->data);
// 반환한 포인터를 언제까지 사용할지 전혀 예측할 수 없으므로 공유 가능 해제
theString.value->markUnshareable();
return &(thsString.value->data[charIndex]);
}
- 문제 2. operator+=, operator++, operator*=, operator<<=, operator-- 등을 사용하는 경우
template<typename T>
class Array
{
public:
class Proxy
{
public:
Proxy(Array<T>& array, int index);
Proxy& operator=(const T& rhs);
operator T() const;
...
};
const Proxy operator[](int index) cosnt;
Proxy operator[](int index);
...
};
Array<int> intArray;
intArray[5] = 22; // 컴파일 성공
intArray[5] += 5; // 컴파일 에러!!
++intArray[5]; // 컴파일 에러!!
- raw 배열처럼 동작시키려면 프록시 클래스에 이를 모두 구현해 줘야 한다.. 매우 할 일이 많다.
- 문제 3. 프록시를 통해 객체의 멤버 함수를 호출할 때
class Rational
{
public:
Rational(int numerator = 0, int denominator = 1);
int numerator() const;
int denominator() const;
...
};
Array<Rational> array;
cout << array[4].numerator(); // 컴파일 에러!!
int denom = array[22].denomitator(); // 컴파일 에러!!
- 원래 객체의 기능에 대한 인터페이스를 프록시에 모두 구현해야 한다..? 끔찍한 일이다.
- 문제 4. 비상수 객체 참조자를 받아들이는 함수의 매개변수로 프록시를 넘기는 경우
void swap(char& a, char& b);
String s = "+C+";
swap(s[0], s[1]); // 컴파일 에러!!
// char로의 암시적 변환만 제공하며, char&로의 암시적 변환은 불가능하다.
// 또한, char는 swap 함수의 char&에 바인딩 될 수 없다.
// char는 임시객체이며, 컴파일러는 임시 객체를 비상수 참조차 매개변수에 바인딩하지 않는다.
// (항목 19 참조)
- 문제 5. 암시적 타입 변환 문제. 2번 이상의 암시적 타입 변환은 컴파일러가 지원하지 않는다.
class TVStation
{
public:
TVStation(in channel);
...
};
void watchTV(const TVStation& station, float hoursToWatch);
watchTV(10, 2.5); // 컴파일 성공
Array<int> intArray;
intArray[4] = 10;
watchTV(intArray[4], 2.5); // 컴파일 에러!!
// Proxy<int> -> int -> TVStation 으로, 암시적 변환이 2회 이상이다.
정리
- 프록시 클래스는 어떤 객체를 대신하여 동작하게 하는 장치이다.
- 다차원 배열, lvalue / rvalue 구분, 암시적 타입 변환 방지 를 프록시 클래스로 해결했다.
- 함수로 반환되는 프록시 객체는 임시 객체(항목 19 참조)이므로 객체 생성, 소멸이 필수적이다.
- 이 비용은 공짜가 아니다.
- 원래의 객체를 사용하는 클래스를 프록시 버전으로 바꾸면, 그 클래스의 의미구조를 어쩔 수 없이 바꿔야 하는 경우가 더러 생긴다.
- 프록시 객체는 대개 원래 객체와 약간 다른 동작패턴을 보이기 때문이다.
- 하지만 이가 드러나는 건 매우 드물고, 보통은 사용자는 별 위화감 없이 프록시 객체를 사용할 수 있다.
728x90
'개발 > More Effective C++' 카테고리의 다른 글
[More Effective C++] 32. 미래 지향적 프로그래밍 (2) | 2024.10.16 |
---|---|
[More Effective C++] 31. 다중 디스패치(multiple dispatch) (0) | 2024.10.07 |
[More Effective C++] 29. 참조 카운팅 (0) | 2024.09.27 |
[More Effective C++] 28. 스마트 포인터 (0) | 2024.09.09 |
[More Effective C++] 27. 힙 전용, 힙 불가 클래스 만들기 (1) | 2024.09.06 |
Comments