Item 22 : Declare data members private
왜 data member가 public이어야 하는가?
1. syntax consistency
만약 멤버 데이터가 public이 아니라면, 클라이언트에 객체에 접근할 수 있는 유일한 방법은 멤버 함수 뿐이다.
모든 public interface가 function이면, 모든 것이 함수이기 때문에 클라이언트는 자연스럽게 함수를 사용할 수 있다.
2. access control
멤버 데이터가 public이 아니라면, 멤버 데이터에 대한 접근 권한이 없는 경우, 읽기 전용, 읽고 쓰기 가능 등 다양한 접근 권한에 대한 구현이 가능하다.
그러나 멤버 데이터가 public이면, 모든 멤버 데이터를 읽고 쓸 수 있다.
모든 데이터 멤버가 hidden이기 때문에 세밀한 접근 권한 제어는 필수적이다.
데이터 멤버들에 대해 getter와 setter가 필요하다.
3. encapsulation
다음과 같이 speed data들에 대한 average값을 가져오는 averageSoFar 함수에 대해 고려해보자.
class SpeedDataCollection{
...
public:
void addVale(int speed);
double averageSoFar() const;
...
};
멤버 함수 averageSoFar을 구현하는 방법은 두가지로 나뉜다.
먼저, 지금까지 수집된 모든 speed data의 평균값을 가지는 데이터 멤버를 만드는 방법으로 구현할 수 있다.
이 방법은 SpeedDataCollection 객체의 크기가 커진다는 단점이 있다.
running average, accumulated total, number of data points 데이터 멤버를 저장할 공간을 할당해야 한다.
따라서, 이 방법은 average 값이 자주 필요하고, 속도가 중요한 경우 적합하다.
다음으로, averageSoFar 함수가 호출될 때마다 speed data의 average를 구하는 방법이다.
함수가 호출될 때 마다 계산하면 속도는 느릴 수 있지만, 추가적인 멤버 데이터를 위한 공간이 필요 없다.
따라서, 메모리 여유 공간이 많이 없고 average값이 자주 필요 없는 경우 적합하다.
멤버 함수를 통해 average에 접근하는 방법은 서로 다른 구현방법들을 원하는 대로 교체할 수 있기 때문에 중요하다.
데이터 멤버를 함수 뒤에 숨기는 것은 모든 종류의 구현에 대한 유연성을 제공할 수 있다.
데이터 멤버를 클라이언트에게 숨기는 것은, 클래스의 불변이 항상 유지된다는 것을 보장할 수 있다.
오직 멤버 함수만이 객체에 영향을 줄 수 있기 때문이다.
또한, 구현 방법을 나중에 변경할 수 있다.
클래스에 대한 모든 멤버 데이터가 public인 경우, 구현을 변경하는 것은 제한된다.
클래스에 대한 코드가 변경되면 많은 클라이언트 코드가 망가진다.
따라서 public은 unencapsulated를 의미하며 즉 변경 불가능하다.
protected data member도 public과 같다.
데이터 멤버를 public또는 protected로 선언하면, 데이터 멤버에 대한 어떤 것도 변경하기 어렵다.
많은 코드가 다시 작성되고 테스트되고 문서화, 컴파일되어야 한다.
4. Conclusion
data member를 private으로 선언해야 한다.
이것은 클라이언트에서 문법적으로 동일한 접근 방식, 세밀한 접근 권한, 불변성을 제공하고 클래스 구현에 대한 유연성을 제공한다.
protected는 public보다 encapsulated의 측면에서 더 좋지 않다.
Item 23 : Prefer non-member non-friend functions to member functions
아래의 코드를 살펴보자.
class WebBrowser{
public:
...
void clearCache();
void clearHistory();
void removeCookies();
...
void clearEverything();
};
void clearBrowser(WebBrowser& wb)
{
wb.clearCache();
wb.clearHistory();
wb.removeCookies();
}
WebBrowser::clearEverything은 멤버 함수, clearBrowser는 non-member function이다.
object-oriented 원칙은 data가 가능한 한 encapsulated되어야 함을 의미한다.
non-member 함수는 encapsulation, packaging flexibility와 클래스의 extensibility를 증가시킨다는 측면에서 더 좋다.
1. encapsulation
더 적은 것을 볼 수 있을수록, 클래스 변경에 대한 유연성이 증가한다.
우리가 볼 수 있는 코드에만 직접적으로 영향을 끼치기 때문에 한정된 수의 클라이언트에만 영향을 준다.
객체가 데이터와 연관된 경우, 볼 수 있는 것이 더 적을수록, 데이터 멤버의 수, 타입 등 더 자유롭게 객체 데이터의 속성을 변경할 수 있다.
private 멤버에 접근할 수 있는 멤버 함수의 수는 멤버 함수와 friend 함수의 수를 더한 것이다.
non-friend, non-member함수는 private 멤버에 접근할 수 있는 멤버 함수의 수를 증가시키지 않기 때문에 encapsulation의 측면에서 더 좋다.
2. extensibility
C++에서 더 자연스러운 접근방법은 non-member 함수 clearBrowser를 WebBrowser의 namespace에 구현하는 것이다.
namespace WebBrowserStuff{
class WebBrowser{ ... };
void clearBrowser(WebBrowser& wb);
...
}
namespace는 클래스와 달리 여러 소스 파일에 확산될 수 있다.
clearBrowser는 편리성 함수이기 때문에 중요하다.
멤버나 friend함수와 달리, non-member, non-friend함수는 webBrowser에 대한 특별한 접근권한이 필요없기 때문에, webBrowser 클라이언트가 가지지 않는 기능을 제공할 수 있다.
WebBrowser 클래스는 bookmarks, printing, cookie management등의 많은 편리성 함수를 가질지도 모른다.
각각의 분류에 대해 서로 다른 헤더 파일에 편리성 함수를 선언할 수 있다.
이것은 standard C++ library가 구성된 방법이다.
std namespace에 모든 헤더가 포함되어 있지만, 여러 헤더파일들에 각각의 기능이 선언되어 있다.
기능들을 이러한 방법으로 나누는 것은 클래스의 멤버함수에서는 불가능하다.
또한, 여러개의 헤더 파일에 편리성 함수들을 만드는 것은 편리성 함수들을 쉽게 확장할 수 있음을 의미한다.
클라이언트는 필요한 편리성 함수에 대한 헤더를 추가하여 확장할 수 있다.
Item 24 : Declare non-member functions when type conversions should apply to all parameters
다음의 코드를 살펴보자.
class Rational{
public:
Rational(int numerator = 0, int denominator = 1);
int numerator() const;
int denominator() const;
private:
...
};
Rational oneHalf(1,2);
Rational result = oneHalf * 2;
result = 2 * oneHalf;
우리의 목표는 mixed-mode arithmetic과 consistency를 지원하는 것이다.
만약 Rational의 생성자가 explicit 생성자라면, 두 코드 모두 컴파일 에러가 발생한다.
그러나, Rational의 생성자가 explicit이 아니라면, 한 코드는 컴파일된다.
Rational oneHalf(1,2);
Rational result = oneHalf * 2; // oneHalf.operator*(2);
result = 2 * oneHalf; // 2.operator*(oneHalf); -- compile error!!
int형 타입은 parameter list에 있기 때문에 파라미터는 implicit type conversion이 된다.
멤버 함수가 호출될 때, 객체에 대응되는 implicit parameter는 implicit type conversion을 통해 처리한다.
그러나, 여전히 두번째 코드는 컴파일 에러가 발생한다.
이를 해결하기 위해 모든 argument에 대한 implicit type conversion을 컴파일러가 수행하도록 operator*를 non-member 함수로 만든다.
const Rational operator*(const Rational& lhs, const Rational& rhs)
{
return Rational(lhs.numerator() * rhs.numerator(), lhs.denominator() * rhs.denominator());
}
정리하면, 함수의 모든 파라미터에 대한 타입 변환이 필요하다면(this 포인터가 가리키는 객체도 포함), 그 함수는 non-member함수여야 한다.
Item 25 : Consider support for a non-throwing swap
예외를 던지지 않는 swap에 대한 지원도 생각해보자.
swap은 표준라이브러리에 구현되어 있다.
namespace std{
template<typename T>
void swap(T& a, T& b)
{
T temp(a);
a = b;
b = temp;
}
}
이 swap 함수는 3개의 객체에 대한 복사를 포함한다는 문제점이 있다.
a를 temp에, b를 a에, temp를 b에 복사한다.
몇몇 타입의 경우, 어떠한 복사도 필요하지 않을 수 있다.
실제 데이터를 포함하는 또다른 타입에 대한 포인터를 포함하는 타입에 대해 고려해보자.
class WidgetImpl{
public:
...
private:
int a,b,c;
std::vector<double> v;
};
class Widget{
public:
Widget(const Widget& rhs);
Widget& operator=(const Widget& rhs)
{
...
*pImpl = *(rhs.pImpl);
...
}
...
private:
WidgetImpl *pImpl; // ptr to object with this
};
우리가 실제로 필요한 것은 pImpl 포인터에 대한 swap이지만, default swap 알고리즘은 이를 수행할 방법이 없다.
대신, default swap알고리즘은 Widget뿐만 아니라 WidgetImpl 객체에 대한 복사를 할 것이다.
따라서 swap가 pImpl 포인터들에 대해서 수행되도록 swap에게 알려준다.
namespace std
{
template<>
void swap<Widget>(Widget& a, Widget& b){
swap(a.pImpl, b.pImpl);
}
}
함수 앞 template<>는 이것이 std::swap에 대한 total template specialization(완전 템플릿 특수화)임을 컴파일러에게 알려준다.
그리고 함수 이름 뒤 <Widget>은 Widget일 경우에 대한 specialization(특수화)이라는 것을 알려준다.
swap이 Widget에 적용될 때는 위의 함수를 사용하게 된다.
일반적으로 std namespace의 내용을 바꾸는 것은 허용되지 않지만, 우리가 만든 타입에 대한 total specialize standard templates는 가능하다.
그러나, 이 함수는 컴파일되지 않는다.
pImpl이 private이기 때문에 이에 접근할 수 없다.
따라서 Widget 클래스에 public member 함수인 swap을 만든다.
class Widget{
public:
...
void swap(Widget& other)
{
using std::swap;
swap(pImpl, other.pImpl);
}
...
};
namespace std{
template<>
void swap<Widget>(Widget& a, Widget& b){
a.swap(b);
}
}
이것은 컴파일된다.
그러나, Widget과 WidgetImpl이 클래스 template인 경우에는 어떻게 구현해야 하는가?
namespace std{
template<typename T>
void swap(Widget<T>& a, Widget<T>& b){
a.swap(b);
}
}
새로운 템플릿을 std에 추가하면 안된다.
따라서, member swap를 호출하는 non-member swap을 선언한다.
이 함수는 std::swap의 오버로딩 또는, 특수화하지 않는다.
namespace WidgetStuff{
...
template<typename T>
class Widget{ ... };
...
template<typename T>
void swap(Widget<T>& a, Widget<T>& b)
{
a.swap(b);
}
}
이 방법은 클래스 뿐만 아니라 클래스 템플릿에서도 잘 동작한다.
만약, class-specific version의 swap을 가능한한 많은 상황에서 호출해야 한다면, 클래스와 같은 namespace에 non-member version의 함수를 선언하고, std::swap에 specialization(특수화)을 위한 코드를 작성해야 한다.
template<typename T>
void doSomething(T& obj1, T& obj2)
{
using std::swap;
...
swap(obj1, obj2);
...
}
컴파일러가 위 함수에서 swap을 호출할 때, 호출할 올바른 swap을 찾는다.
먼저, type T가 위치하는 namespace 또는 global space에서 T-specific swap을 찾는다.
만약 T-specific swap이 존재하지 않는다면, 컴파일러는 std의 swap을 사용할 것이다.
아래와 같이 내용을 다시 정리할 수 있다.
1. 만약, swap의 default 버전이 클래스나, 클래스 템플릿에 효율적으로 동작한다면, 아무것도 안해도 된다.
2. 만약, swap의 default 버전이 충분히 효율적이지 않다면, 다음과 같은 사항을 따른다.
1) public swap member function을 제공한다. 이 함수는 너의 타입의 투 객체의 값을 효율적으로 바꾼다.
2) 너의 클래스나 템플릿과 같은 namespace에 non-member swap를 제공한다. 이 함수는 swap member function을 호출한다.
3) 만약, 클래스를 클래스 템플릿이 아닌 버전으로 작성했다면, 너의 클래스에 대한 std::swap를 특수화한다. 이 함수도 swap member function을 호출한다.
마지막으로, swap을 호출할 때, std::swap에 대해 using 선언을 사용한 다음 swap을 호출한다.
'C++ > Effective C++' 카테고리의 다른 글
[Effective C++] Chapter 5: Implementations(2) (0) | 2022.07.06 |
---|---|
[Effective C++] Chapter 5: Implementations(1) (0) | 2022.07.05 |
[Effective C++] Chapter 4: Design and Declarations(1) (0) | 2022.07.02 |
[Effective C++] Chapter 3: Resource Management (0) | 2022.07.01 |
[Effective C++] Chapter 2: Constructors, Destructors, and Assignment Operators(2) (0) | 2022.06.30 |