3. Item 18: 잘쓰기 쉽게 못쓰기 어렵게
• 인터페이스
• 유저와 코드가 접하는 경로
• 잘쓰기는 쉽게 못쓰기는 어렵게 만들자
• 유저가 생각하는 대로 동작하게 하자.
• 잘못 쓴 경우 뭔가 항의의 몸부림을 치자.
• 사용자가 실수하는 경우를 머리에 두고 있자.
4. Item 18: 잘쓰기 쉽게 못쓰기 어렵게
• 날짜 클래스의 생성자
Date( int month, int day, int year);
• 매개변수 순서를 잘못 입력한 경우
Data d(30, 3, 1995); //3, 30이 올바른 사용
• 가능한 범위를 넘어가는 경우
Data d(3, 40, 1995); //3월 40일은 없다.
5. Item 18: 잘쓰기 쉽게 못쓰기 어렵게
• Day, Month, Year 각각 wrapper 타입을 만들자.
• struct Day {explicit Day(int day)…} //Month, Year 이하 동일
• Data (const Month& m, const Day& d, const Year& y);
• Date d(30, 3, 1995); //타입 체크 오류
• Data d(Day(30), Month(3), Year(1995)); //타입 체크 오류
• Data d(Month(3), Day(30), Year(1995); //올바른 사용
6. Item 18: 잘쓰기 쉽게 못쓰기 어렵게
• 타입의 값에 제약을 가해보자.
• 유효한 Month는 12개 뿐
• Enum ? 타입 안전성이 불안해 (http://ozt88.tistory.com/15 참고)
• static 함수로 Month를 반환하는 방식을 사용하자. (item 4참고)
public:
static Month Jan(){return Month(1); … //Month를 만드는 유일한 창구
private:
explicit Month(int m); //생성자를 private해서 안전하게
7. Item 18: 잘쓰기 쉽게 못쓰기 어렵게
• 또 다른 제약 붙이기 const (item 3 참조)
• operator*의 반환 타입을 const로 한정한 이유
• if( a * b = c )… //이런 실수를 막기 위해서
• 별다른 이유가 없다면 클래스는 기본 타입처럼 동작하게 하자
• 기존 int 역시 *연산에 다른 값을 대입할 수 없게 만들어져있음.
• 유사한 쓰임새의 기본타입의 동작을 모방하자
• 일관성 있는 인터페이스의 제공
8. Item 18: 잘쓰기 쉽게 못쓰기 어렵게
• STL에서 나타나는 비일관성의 예
• 데이터의 길이 또는 크기를 나타낼 떄
• Array는 length 프로퍼티로
• String은 length 메서드로
• List는 size 메서드로
• 비 일관성은 인터페이스를 해친다.
9. Item 18: 잘쓰기 쉽게 못쓰기 어렵게
• 사용자 쪽에서 뭔가 외워야 하는 인터페이스는 잘못 되었다.
• Investment* createInvestment(); //팩토리 함수 (in item 13)
• 사용자 측에서 delete하는 것을 잊어서는 안 된다.
• 사용자 측에서 별 고민 없이 그냥 사용할 수 있게
• std::shared_ptr<Investment> createInvestment(); //스마트포인터 반환
• 자동 해제되어 편하게 사용가능
• 사용자 실수를 원천 봉쇄 ( deleter도 설계자가 지정 가능 )
10. Item 18: 잘쓰기 쉽게 못쓰기 어렵게
• 사용자 쪽에서 뭔가 외워야 하는 인터페이스는 잘못 되었다.
• Investment* createInvestment(); //팩토리 함수 (in item 13)
• 사용자 측에서 delete하는 것을 잊어서는 안 된다.
• 사용자 측에서 별 고민 없이 그냥 사용할 수 있게
• std::shared_ptr<Investment> createInvestment(); //스마트포인터 반환
• 자동 해제되어 편하게 사용가능
• 사용자 실수를 원천 봉쇄 ( deleter도 설계자가 지정 가능 )
11. Item 18: 잘쓰기 쉽게 못쓰기 어렵게
• 교차 DLL 문제
• 자원 할당된 포인터가 여기저기 돌아다니는 경우
• 객체 생성시에 특정 DLL에 있는 new를 사용
• 해제 시에는 다른 DLL에 있는 delete 를 사용
• tr1::std::shared_ptr은 그런 걱정 ㄴㄴ
• Shared_ptr은 자신의 deleter를 control block에서 관리하기 때문
• 훌륭한 인터페이스의 대표적 사례 (http://ozt88.tistory.com/28 참조)
13. Item 19: 클래스 설계는 타입을 설계하듯
• 클래스를 설계할 때, 새로운 언어의 타입을 설계하듯
• 자연스러운 구문, 의미체계의 구축
• 직관적이며 동시에 효율적인 기능 구현
• 고려사항들 1
• 객체 생성 소멸 방식
• 객체 초기화와 대입연산의 차이
• 값 복사의 의미와 구문
14. Item 19: 클래스 설계는 타입을 설계하듯
• 고려사항들 2
• 타입의 domain 설정
• 값 제약
• Setter들에서 조건 체크
• 멤버 상속여부
• 상속 받았다면 어떤 멤버의 속성을 상속받아 사용할 것인가?
• 상속해 줄 것이라면 어떤 멤버를 가상으로 설정할 것인가?
15. Item 19: 클래스 설계는 타입을 설계하듯
• 고려사항들 3
• 타입 변환
• 어떤 타입들로 변환을 허락할 것인가
• 암시적 변환/ 명시적 변환
• 적당한 연산자와 멤버 함수 선정
• special function 중 어떤 것을 취사 선택하여 사용할 것인가?
• Private로 봉인 가능
• 멤버함수 접근 권한 설정 (public/private/protected)
16. Item 19: 클래스 설계는 타입을 설계하듯
• 고려사항들 4
• 선언되지 않은 인터페이스
• 보장할 자원, 성능, 예외 안정성
• 일반적 템플릿을 만들어야 하는가?
• 정말로 필요한 타입인가?
• 비슷한 클래스가 있고 기능이 몇 개 추가되지 않는다면
• 일반 비멤버 함수를 만들거나 템플릿을 몇 개 더 정의하는 게 낫다.
18. Item 20: 객체 전달은 const T&
• C++ 함수의 동작은 기본적으로 값에 의한 전달
• 일반 객체 paramete는 argumen에 대한 사본으로 생성
• 반환 값의 사본을 만들어 반환 객체를 받는다.
• 예제 코드 Person class와 그를 상속받는 Student Class
20. Item 20: 객체 전달은 const T&
• Student를 인자로 전달받는 함수 validateStudent
bool validataStudent(Student s);
…
Student plato;
bool platoIsOK = validateStudent(plato);
• 객체 전달에 필요한 비용
• Parameter의 Student 복사 생성자 호출
• Argument의 Student 소멸자 호출
21. Item 20: 객체 전달은 const T&
• 좀더 구체적인 비용
• Student 생성할 때 덩달아 불리는 생성자들
• 멤버변수 SchoolName 생성자, SchoolAddress 생성자 호출
• 상속받은 Person의 생성자 호출
• Person의 멤버변수 address, name 생성자 호출
• Student 해제할때 덩달아 호출되는 소멸자들
• 위에서 생성된 모든 객체들 각각 해제
• TOTAL : 생성자 6번, 소멸자 6번
22. Item 20: 객체 전달은 const T&
• 비효율적인 값 전달의 대체제 const T&
• Reference-to-Const
• 어차피 값으로 전달하면, 기존 객체의 값이 바뀔 일이 없다는 의미
• 상수성이 유지되면 사본을 만드는 대신 상수 객체의 참조를 넘기는게 이득
• No 생성 No 소멸 No 복사
• bool validateStudent( const Student& s );
• 복사손실 문제를 해결
• 정적 타입이 부모 클래스인 경우, 부모클래스의 복사 생성자 호출로 데이터 손실
• Window 클래스와 그것을 상속받은 WindowWIthScrollBars 클래스 예제
24. Item 20: 객체 전달은 const T&
• 윈도우의 이름을 출력하고 윈도우를 화면에 출력하는 함수
• void printNameAndDisplay(window w)
• 여기에 WindowWithScrollBars 객체를 전달
• WindowWithScrollBars wwsb;
printNameAndDisplay(wwsb); //복사 손실발생!
• 여기에서도 해결책은 Reference-to-Const
• void printNameAndDisplay(const Window& w);
• 기존 매개변수의 참조만 사용하므로 데이터를 유지한 상태로 사용됨.
25. Item 20: 객체 전달은 const T&
• const T&가 만능은 아니다.
• 참조자는 근본적으로는 포인터로 구현된다.
• 전달 타입이 기본(primitive)타입인 경우 값 전달이 효율적이다.
• STL의 반복자와 함수객체에서도 동일한 convention이 적용
• 반복자와 함수객체의 복사 생성자/할당자의 효율을 고려해야 한다.
• 복사 손실 문제를 해결해야 한다.
26. Item 20: 객체 전달은 const T&
• 기본 타입이 아닌 값 전달의 위험성
(크기 작다고 값 복사 하지마)
• 객체 크기가 작아도 복사 비용이 적은 것은 아님
• 포인터 타입의 깊은 복사의 경우
• 사용자 정의 타입은 연산 자체가 다르다.
• Double 하나만 있는 객체와 Double을 다루는 방식이 다름
• 기본 double은 레지스터 하나로 처리, 하지만 객체는 다르다!
• 포인터/참조는 무조건 레지스터 하나에 들어감
• 사용자 정의 타입의 크기는 변화가능하다.
28. Item 21: 함수 반환에서 참조 반환을 피하자.
• 참조 전달 만능주의에 빠지지 말자.
• 반환형이 참조일 때, 이미 소멸된 객체의 참조를 반환할 가능성
• Rational 클래스에서 friend 함수 operator*의 예시
class Rational {
…
friend
const Rational operator* (const Rational& lhs, const Rational& rhs);
}
29. Item 21: 함수 반환에서 참조 반환을 피하자.
• 곱셈 결과를 값으로 반환하는 것이 정당한가?
• 참조자를 반환하면 생성과 소멸에 드는 비용을 줄일 수 있다.
• 참조자는 alias 즉 이미 존재하는 객체에 대한 참조
• 참조로 반환하는 경우 반환된 객체가 따로 어딘가에 존재해야 한다!
• Rational을 참조로 반환하고 싶다면
• Operator*에서 Rational 객체를 따로 생성해야 한다.
• 힙에 할당하거나, 스택에 생성하거나, 또는 static(^^)
30. Item 21: 함수 반환에서 참조 반환을 피하자.
• 스택에 생성하는 경우
const Rational& operator* (const Rational& lhs,
const Rational& rhs)
{
Rational result(lhs.n * rhs.n, lhs.d * rhs.d); //반환값 스택에 생성
return result;
}
• 생성자 호출을 피하기위한 참조 반환이었지만 결국엔 생성해야한다.
• 반환된 참조의 대상은 함수 종료 후 소멸된다. (댕글링 참조자)
31. Item 21: 함수 반환에서 참조 반환을 피하자.
• 힙에 생성하는 경우
const Rational& operator* (const Rational& lhs,
const Rational& rhs)
{
Rational* result = new Rational(lhs.n * rhs.n, lhs.d * rhs.d);
return *result;
}
• 여전히 생성자 호출의 비용은 그대로
• 할당한 메모리 해제를 어떻게 할 것인가?
32. Item 21: 함수 반환에서 참조 반환을 피하자.
• 힙에 생성한 객체의 참조반환
• Rational w, x, y, z;
w = x * y * z; //operator*(operator*(x, y), z);
• 사용자 측에서 반환되는 참조자의 포인터에 접근할 방법이 없다.
• 함수 내에서는 해제하면 안 된다. (댕글링 참조자 문제)
• delete는 어디서 누가 어떻게 왜!!?
33. Item 21: 함수 반환에서 참조 반환을 피하자.
• 마지막 발악. static으로 반환할 Rational을 정의한 경우
const Rational& operator* (const Rational& lhs,
const Rational& rhs)
{
static Rational result(lhs.n * rhs.n, lhs.d * rhs.d);
return result;
}
• Rational a, b, c, d;
If( (a * b) == (c * d) ) … //반환값이 static에 대한 참조이므로 항상 true
34. Item 21: 함수 반환에서 참조 반환을 피하자.
• 가능하면 그냥 값 전달하자. 물론 정석은 있다.
inline const Rational operator* (const Rational& lhs,
const Rational& rhs)
{return Rational ( lhs.n * rhs.n, lhs.d * rhs.d );}
• inline 키워드를 사용하여 불필요한 생성/소멸자 호출 방지
• 컴파일러 최적화에 맡기자.
36. Item 22: 데이터 멤버들은 private에 선언하자
• Public 데이터 멤버 무엇이 문제인가?
• 사용자의 문법적 일관성을 해친다.
• 어떤 멤버는 데이터고 어떤 멤버는 함수다?
• 사용자가 객체에 접근 가능한 방법이 모두 함수인 편이 일관성이 높다.
• 사용자의 데이터 접근을 제어할 수 없다.
• 데이터 멤버에 대해 자유로운 접근이 가능하다.
• Private하면 직접 함수로 접근 방식을 커스터마이징 할 수 있다.
37. Item 22: 데이터 멤버들은 private에 선언하자
• Public 데이터 멤버 무엇이 문제인가? 2
• 클래스의 온전한 캡슐화를 할 수 없다.
• 클래스 캡슐화의 좋은 예
class SpeedDataCollection {
public:
void addValue(int speed); //새로운 데이터 추가.
double averageSoFar() const; //평균 속도 반환
}
38. Item 22: 데이터 멤버들은 private에 선언하자
• SpeedDataCollection 예시
• averageSoFar() 함수 어떻게 구현할 것인가?
• 속도 평균값을 저장하는 데이터 멤버를 유지하고 그 값을 반환한다.
• 성능 향성 but 객체 비대화
• 호출할 때마다 평균을 계산하여 반환한다.
• 성능 구림 but 작은 사이즈 객체
• 상황에 따라 적합한 방식으로 구현해야 한다.
39. Item 22: 데이터 멤버들은 private에 선언하자
• SpeedDataCollection 예시
• 평균값에 대한 캡슐화가 이루어지지 않았다면?
• 구현방식이 바뀔 때마다 접근방식도 변경될 것이다.
• 캡슐화를 통해서 외부 객체는 항상 동일한 방식으로 원하는 결과를 받을 수 있다.
• 구현상의 융통성을 위한 캡슐화
• 함수를 통해 전달하기 때문에 데이터 전달 시 이벤트/동기화등 다양한 작업이 가능
• Public으로 선언된 멤버들을 변경하면 접근하는 방식을 손 봐야 한다는 의미
• 캡슐화는 클래스의 불변속성을 유지하는데 유리하다.
• 유일한 통로를 통해 접근하므로, 함부로 변경되는 것을 막을 수 있다.
40. Item 22: 데이터 멤버들은 private에 선언하자
• Protected의 데이터 멤버 무엇이 문제인가?
• Public과 동일한 상황
• 데이터 멤버가 바뀌면 (제거되면) 깨지는 코드의 양과 캡슐화는 반비례
• 결국 protected인 데이터 멤버를 사용하는 모든 파생클래스들이 의존상태인 것.
• 부모 클래스의 protected인 데이터 멤버가 변경되면 많은 코드가 변경돼야 한다.
• 진짜 캡슐화를 하고 싶으면 데이터 멤버들은 private해라!
42. Item 23: 비멤버 비프렌드 함수의 장점
• WebBrowser 클래스의 사례
• clearCache, History, Cookie를
한번에 하고 싶다면?
• 함수 clearEverything();
• 멤버 함수로 할 것인가?
• 비멤버 함수로 할 것인가?
class WebBrowser {
public:
...
void clearCache();
void clearHistory();
void removeCookies();
};
43. Item 23: 비멤버 비프렌드 함수의 장점
• 캡슐화의 측면
• 캡슐화는 데이터에 접근 가능한 루트가 적을 수록 증가한다.
• 멤버함수는 객체의 private 데이터에 접근 가능하다.
• 비멤버 + 비프랜드 함수는 private 데이터에 접근 불가능하다.
• 캡슐화를 강화하는 것은 비멤버 + 비 프렌드 함수를 사용하는 것!
• 다른 비 프랜드 객체의 멤버함수를 사용하는 것도 훌륭한 선택이다.
44. Item 23: 비멤버 비프렌드 함수의 장점
• 비 멤버 함수와 해당 객체를 같은 네임스페이스 안에 두자
• Namespace WebBrowserStuff {
class WebBrowser {…};
void clearBrowser (WebBrowser& wb);
}
• Utility 함수를 다루는 매우 훌륭한 방법
• 같은 네임스페이스를 여러 개의 소스로 나누어서 사용하는 것이 가능하다.
• 다양한 utility 기능들을 기능별로 헤더로 분리하여 선언하는 것이 가능
45. Item 23: 비멤버 비프렌드 함수의 장점
• //in WebBrowser.h
Namespace WebBrowserStuff {
class WebBrowser { … };
}
//in WebBrowserBookmarks.h
Namespace WebBrowserStuff {
…
}
• Std 표준 라이브러리도 이런
형식으로 구성된다.
• 필요한 기능만 include 해서
사용할 수 있도록
• 실제로 사용하는 요소만 컴파
일 의존성을 고려하면 된다.
• 패키징 유연성!
46. Item 23: 비멤버 비프렌드 함수의 장점
• Utility 함수 집합의 확장
• 해당 네임스페이스에 비멤버 비프랜드 함수를 추가하면 됨
• 새로운 헤더 만들고 네임스페이스에 원하는 함수들 추가하면 끝
• 캡슐화가 잘되고 확장도 쉬운 비멤버 비프렌드 함수 씁시다.
48. Item 24: 멤버함수의 Blind Sight
• 다시 Rational class (지겹다)
• 곱셈 연산을 지원하고 싶다.
• 멤버함수 / 비멤버 함수/ 비멤버 프렌드 함수 (선택지가 너무 많아)
• 객체 지향 파워로 돌파해보자.
• 곱셈 연산은 Rational 클래스 자체랑 관련이 있으니까
• 멤버함수로 넣어주는 게 적절할 것만 같아.
• Item 23에서는 캡슐화가 어쩌고 했지만 넘어가도록 하자
49. Item 24: 멤버함수의 Blind Sight
• const Rational operator* (const Rational& rhs);
• Rational oneEight (1, 8);
Rational oneHalf (1, 2);
Rational result = oneHalf * oneEight;
result = result * oneEight;
• 자~알 돌아간다.
50. Item 24: 멤버함수의 Blind Sight
• const Rational Rational::operator* (const Rational& rhs);
클래스 멤버함수로 operator*를 정의하자
• Rational oneEight (1, 8);
Rational oneHalf (1, 2);
Rational result = oneHalf * oneEight;
result = result * oneEight;
• 자~알 돌아간다.
51. Item 24: 멤버함수의 Blind Sight
• 혼합형 수치 연산하고 싶다. 헠헠 Int * Rational 같은거…
result = oneHalf * 2; //OK
result = 2 * oneHalf //에러?! 왜?!
• 문제의 원인
result = oneHalf. operator*(2); //Rational에서 메소드 호출
result = 2. operator*(oneHalf); //Int에서 메소드 호출??
52. Item 24: 멤버함수의 Blind Sight
• 다른 방법의 돌파구
result = operator*(2, oneHalf); //에러!?
• Rational::operator*는 좌변에 반드시 this를 사용하기 때문!
• 우변은 암시적 형변환으로 자연스러운 처리
Rational temp(2);
result = oneHalf.operator*( temp );
• 생성자가 explicit였으면 형변환 불가능으로 이마저도 에러
53. Item 24: 멤버함수의 Blind Sight
• 양변에 모두 암시적 형변환을 지원하고 싶다면?
• 멤버 함수로는 안된다.
• 비멤버 함수로 처리
const Rational operator*(const Rational& lhs, const Rational& rhs);
{//좌변과 우변을 모두 설정가능
return Rational(lhs.numerator() * rhs.numerator(), //캡슐화된 접근
lhs.denominator() * rhs.denominator());
}
54. Item 24: 멤버함수의 Blind Sight
• 완벽하게 캡슐화된 접근방식으로 operator*를 구현가능하다.
• 쓸데없이 friend 선언해서 캡슐화를 줄이지 말자.
• 객체를 다루는 함수는 가능하면 비 friend 함수로 캡슐화된 접근을 통
해 문제를 해결하자.
56. Item 25: 예외 처리 없는 swap
• Swap의 중요성
• 예외 안전성 프로그래밍에 반드시 필요한 역할 수행
• 자기 대입 현상에 대처하는데 핵심적 역할 (item 11참조)
• STL에 추가됨
• 잘 동작하는 swap을 만들어 두자.
57. Item 25: 예외 처리 없는 swap
• 표준의 std::swap
• 전형적인 구현
• T가 복사연산만 지원한다면 제
대로 동작한다.
• 세 번의 복사 연산 비용
• 썩 훌륭해 보이지 않는다.
• Template<typename T>
void swap(T& a, T& b)
{
T temp(a);
a = b;
b = temp;
}
58. Item 25: 예외 처리 없는 swap
• Pimple Idiom 사용하는 객체의 swap
• Pimple Idiom은 (http://ozt88.tistory.com/32 참조)
• Swap에서 Impl*의 포인터 pImpl의 주소값만 맞바꿔주면 충분하다.
• 하지만 std::swap의 코드대로라면 깊은 복사를 3번 수행한다.
• 초 비효율
59. Item 25: 예외 처리 없는 swap
• std::swap에 템플릿 특수화를 더하여 해결하자
• Pimple Idiom을 사용하는 객체 Widget에 swap() 메소드 준비
• template<>
void swap<Widget>(Widget& a, Widget& b){
a.swap(b);
}
60. Item 25: 예외 처리 없는 swap
• 좀더 확장해보자.
• Pimpl에 들어가는 데이터 타입을 매개변수로 받는 클래스 템플릿
• Widget<T>
template<typename T>
void swap<Widget<T>>(Widget<T>& a, Widget<T>& b)
{ a. swap( b ); } //에러??!!
• 함수 템플릿에서 부분 특수화를 허용하지 않는다.
• 클래스 템플릿은 허용한다.
61. Item 25: 예외 처리 없는 swap
• 부분 특수화 대신 swap을 오버로딩 하는 방법으로 해결가능
template<typename T>
void swap(Widget<T>& a, Widget<T>& b)
{ a.swap(b); } //템플릿 특수화를 버리고 일반 오버로딩을 사용
• 하지만 이 코드는 제대로 동작하지 않는다.
• std에 새로운 함수를 추가하는 것이 불가능하기 때문
• std 밖에서도 쉽게 사용할 수 있는 방법은?
62. Item 25: 예외 처리 없는 swap
• Widget<T>와 오버로딩할
swap함수를 같은 네임스페이스
에 선언한다.
• 컴파일러의 이름탐색 규칙에 따
라서 같은 네임스페이스 안에
있는 swap이 먼저 호출된다.
namespace WidgetStuff {
template<typename T>
class Widget { … };
…
template<typename T>
void swap(Widget<T>& a ,
Widget<T>& b);
…
}
63. Item 25: 예외 처리 없는 swap
• 사용자가 swap을 쓰려면?
• 타입 전용 swap이 있는지 없는지도
모르는 상황
• 오른쪽 처럼 쓰면 OK
• using std::swap
• 표준 swap쓸 수 있게 준비
• 실제 swap 호출에서 먼저 이름탐색 규
칙에 따라 T나 네임스페이스에 맞는
swap을 먼저 찾고 없으면 std::swap 씀
Template<typename T>
void doSomething(T& ob1, T&
ob2)
{
using std::swap;
…
swap(ob1, ob2);
}
64. Item 25: 예외 처리 없는 swap
• 요점정리
• Swap 효율이 나쁘지 않으면 그냥 써라
• 따로 Swap을 만들어 쓰고싶다면
• 해당 타입의 public 멤버함수로 원하는 swap을 정의해라
• 클래스 또는 템플릿이 있는 네임스페이스에 멤버함수 swap을 호출하는
비멤버 swap을 만들어 넣어라.
• 일반 클래스에대한 특수화는 std::swap에서 템플릿 특수화하는 것으로 충분하다.
• swap 호출전에 using std::swap;을 선언하자.