3. Item 18 : 인터페이스 설계를 잘하자
인터페이스?
함수, 템플릿, 클래스 등등 코드와 사용자가 만나는 지점
- 어떤 인터페이스를 쓰고 컴파일 되었다면 사용자가 생각 한대로 움직여야 한다
인터페이스, 제대로 쓰기는 쉽게, 엉터리로 쓰기는 어렵게 하는 법을 알아보자
4. Item 18 : 인터페이스 설계를 잘하자
1. 타입을 이용하자
미사일의 데미지를 설정하고 싶다고 하자.
만약 데미지가 항상 1,3,5,7의 값만 가져야 한다면 어떨까?
void setDamage(int damage); //항상 1,3,5,7만 쓰시오. 이렇게 주석으로?
Enum MissileDamage{ …} // 여기서 1,3,5,7 값만 정의
void setDamage(MissileDamage damage); // 제한된 값만 이용가능
5. Item 18 : 인터페이스 설계를 잘하자
1. 타입을 이용하자
미사일의 데미지를 설정하고 싶다고 하자.
만약 데미지가 항상 1,3,5,7의 값만 가져야 한다면 어떨까?
void setDamage(int damage); //항상 1,3,5,7만 쓰시오. 이렇게 주석으로?
Enum MissileDamage{ …} // 여기서 1,3,5,7 값만 정의
void setDamage(MissileDamage damage); // 제한된 값만 이용가능
const도 적극 이용하자.
6. Item 18 : 인터페이스 설계를 잘하자
2. 그렇게 하지 않을 이유가 없다면 기본 제공 타입처럼 만들자
사용자에게 가장 익숙한 타입은 기본 제공 타입이다. 일관성은 인터페이스
설계의 중요한 덕목이다. 사용자가 뭔가 외워서 사용해야 하는 인터페이스는
잘못 쓰기가 쉽다.
7. Item 18 : 인터페이스 설계를 잘하자
3. 자원관리를 위해서 스마트 포인터를 사용하자
Missile* createMissile();
Missile을 내부적으로 할당해 그 포인터를 반환하는 팩토리 함수이다.
Missile의 사용이 끝나면 해당 객체를 할당 해제 해줘야 한다.
하지만 사용자는 해제 안 해줄수도, 두 번 해줄수도 있다. 여러 문제를 피하기 위해
스마트포인터를 적극적으로 사용하자.
std::unique_ptr<Missile> createMissile();
// 받는 쪽에서 unique로 할지 shared로 할지 정할 수 있다. 해제도 자동으로 한다.
9. Item 19 : 클래스 설계 == 타입 설계
새로운 클래스를 정의한다 == 새로운 타입을 정의한다
좋은 클래스 설계란?
- 문법(syntax)이 자연스럽고, 의미구조(semantics)가 직관적이며, 효율적인 구현이 한
가지 이상 가능해야 한다.
10. Item 19 : 클래스 설계 == 타입 설계
좋은 클래스 설계를 위한 질문
1. 새로 정의한 타입의 객체의 생성 및 소멸은 어떻게 이루어져야 하는가?
클래스의 생성자 및 소멸자의 설계와 연관, operator new, delete등과 연관
2. 객체 초기화는 객체 대입과 어떻게 달라야 하는가?
3. 새로운 타입으로 만든 객체가 값에 의해 전달되는 경우에 어떤 의미를 줄 것인가?
4. 새로운 타입이 가질 수 있는 적법한 값에 대한 제약으로 무엇을 잡을 것인가?
항상 유지되어야 하는(invariant) 값들의 관리, 멤버 변수 “쓰기” 작업에 대한 고려
5. 기존의 클래스 상속 계통망(inheritance graph)에 맞출 것인가?
이어서…
11. Item 19 : 클래스 설계 == 타입 설계
6. 어떤 종류의 타입 변환을 허용할 것인가?
암시적, 명시적, 함수를 통해서, 무엇으로 변할 수 있게 할 것인가
7. 어떤 연산자와 함수를 두어야 의미가 있을까?
8. 표준 함수들 중 어떤 것을 허용하지 말 것인가?
9. 새로운 타입의 멤버에 대한 접근 권한을 어느 쪽에 줄 것인가?
public, protected, priavte의 문제
10. ‘선언되지 않은 인터페이스’로 무엇을 둘 것인가?
클래스가 보장할 수 있는 수행성능, 예외 안전, 자원사용등의 문제
이어서…
12. Item 19 : 클래스 설계 == 타입 설계
11. 새로 만든 타입이 얼마나 일반적인가?
템플릿을 이용해서 만들어야 하는가?
12. 정말로 꼭 필요한 타입인가?
비멤버함수나, 템플릿을 통해 만들 수 없는가?
13. Item 20
`값에 의한 전달`보다는
`상수객체 참조자에 의한 전달`
방식을 택하는 편이 대개 낫다
14. Item 20 : 웬만하면 상수객체 참조자에 의한 전달 방식을 쓰자
`값에 의한 전달`?
함수로부터 객체를 전달 받거나 전달 할 때
매개변수가 전달 인자의 `사본`에 의해 초기화 되는 것
누누이 이야기 들어온 예, 만약 엄청 큰 객체의 사본 만들기 x 2000000
비효율의 비효율의 비효율이다.
그 객체가 상속-> 상속-> 상속 이었다면
각 사본이 만들어질 때마다 상속받은 모든 클래스에 대한 객체들이
파바박 하고 만들어진다.
15. Item 20 : 웬만하면 상수객체 참조자에 의한 전달 방식을 쓰자
`상수객체 참조자` const T&
bool validateMissile(const Missile& m); // 유효한 미사일인지 확인하는 함수
정말로 복사되어야 하는 상황이 아니라면!
해당 타입의 멤버 변수들의 확인해서 뭔가 작업하는 것이라면!
const로 값을 바꾸지 않게 제약을 두고 `사본`을 만들지 않도록 참조를 사용하는
것이다.
16. Item 20 : 웬만하면 상수객체 참조자에 의한 전달 방식을 쓰자
`상수객체 참조자` const T&
또 `복사 손실의 문제`도 막을 수 있다.
복사손실의 문제는 파생 클래스 객체가 기본 클래스 객체로서 전달되는 경우에
기본 클래스의 복사 생성자가 호출되면서 파생클래스의 멤버변수가 떨어지게 되는
상황을 말한다. 상수객체는 그 자신을 넘기므로 이런 문제에서 해방된다.
17. Item 20 : 웬만하면 상수객체 참조자에 의한 전달 방식을 쓰자
`값에 의한 전달`을 사용할 수도 있는 경우
STL 반복자나 함수 객체의 경우 예전부터 값으로 전달 되도록 설계 되었다.
다만 반복자와 함수 객체를 구현 할 때 반드시
1. 복사효율을 높일 것
2. 복사손실 문제에 노출되지 않도록 할 것
위 두 항목이 요구된다.
또 기본 타입의 경우 내부적으로 포인터를 사용하는 참조에 비해
값에 의한 전달로 더 적은 메모리를 사용할 수 도 있다.
18. Item 20 : 웬만하면 상수객체 참조자에 의한 전달 방식을 쓰자
기본타입이 값에 의한 전달을 사용할 수 있으니 하게 되는 착각
1. 내가 만든 타입도 작기만 하면 괜찮지 않나?
만약 내가 포인터 하나만 들고 있는 경우 어짜피 사용하는 메모리 크기도 같은데
값에 의한 전달을 하면 안되나 하고 생각할 수 있지만 오산이다. 왜냐하면 복사 할
때 그 포인터가 들고 있는 객체를 복사해줘야 할테니까
2. 또 어떤 컴파일러는 사용자 정의 타입과 기본 타입을 다르게 취급하기도 한다.
이 때문에 메모리 크기만 덜 사용한다고 사용자 정의 타입을 `값에 의한 전달`로
마구 넘겨서는 안된다.
마지막으로 사용자 정의 타입은 언제나 그 크기가 변할 수 있으므로 항상
`값에 의한 전달`을 주의 해야 한다.
20. Item 21 : 함수에서 객체를 참조자로 반환하지 말자
앞서 20에서 다룬 것은 `전달`의 문제고 이번은 `반환`의 문제이다.
`값에 의한 전달`을 지양 했으니 모든 부분에 `값에 의한 전달`을 지우고 싶다!!
라고 생각하면 큰일이다.
값에 의한 전달이 아닌 방식으로 어떤 객체를 반환하려고 할 때
취할 수 있는 방법 두 개를 소개 하고 문제점을 살펴 보겠다.
21. Item 21 : 함수에서 객체를 참조자로 반환하지 말자
1. 지역객체 참조자로 반환하기
const Missile& createMissile()
{
Missile m();
return m;
}
안돼.. 으윽.. 이러면 안돼!
지역 객체로 생성된 m은 그저 그 block 안에서만 살아 있을 뿐이야!!
m을 반환 받은 애는 m을 써보지도 못할꺼야..ㅜ.ㅜ
22. Item 21 : 함수에서 객체를 참조자로 반환하지 말자
2. new로 생성하고 참조자로 반환하기
const Missile& create();
{
Missile* m = new Missile();
return *m;
}
으… 안돼
저 new로 할당된 Missile은 누가 언제 어떻게 delete 해주지??
ㅜ.ㅜ
23. Item 21 : 함수에서 객체를 참조자로 반환하지 말자
그러니까 객체를 복사해서 넘겨줘야 할 때는 복사하자
const Missile create();
{
return Missile();
}
참조자를 반환할 것인가, 객체를 반환할 것인가를 결정할 때의 원칙
올바른 동작이 이루어지는가?
비용의 문제는 컴파일러에게 맡겨두자
25. Item 22 : 데이터 멤버는 private 영역에 선언하자
왜 안 public, protected요?
1. 문법적 일관성의 문제
- 어떤 클래스는 멤버변수에 접근할 때 그냥 접근하고 어떤 클래스는 함수로 접근하게
하지 말자. 다 똑같은 방법(함수)로 접근하게 하자
2. 읽기 쓰기 접근 권한을 클래스 작성자가 제어할 수 있다.
- 함수를 만들어 주는 것으로 제어 가능
3. 캡슐화
어떤 데이터 멤버를 얻는데 다른 작업을 거쳐야 한다면 해당 작업을 마친 후 전달
할 수 있다.
26. Item 22 : 데이터 멤버는 private 영역에 선언하자
C++의 public은 캡슐화 되지 않았다는 것을 의미한다.
실질적으로 캡슐화 되지 않았다는 것은 `바꿀 수 없음`을 의미
`바꾸면 안됨`을 의미한다고 생각하는게 더 쉬울지도…
널리 쓰이는 클래스일수록 해당 캡슐화 해서 사용해야 한다.
일례로 어떤 클래스의 public 데이터를 제거한다고 할 때를 생각해 보자.
이 멤버에 매달려 있는 수 많은 코드들은 망가지고 만다. 캡슐화 되지 않은 멤버변수
가 이렇게 위험하다.
protected는 뭐가 다른가?
그렇지 않다. 바로 위의 예에서 public을 protected로 바꿔보라. 같은 결과가 나온다.
28. Item 23 : 멤버 함수보다는 비멤버 비프렌드 함수
캡슐화?
어떤 것을 캡슐화하면, 외부에서 이것을 볼 수 없게 된다.
캡슐화 = 캡슐화 대상을 바꾸는 유연성 증가, 캡슐화 볼 수 있는 대상 감소
어떤 멤버 변수에 접근하는 함수의 개수 증가 = 개략적인 캡슐화 정도 감소
public 상태면 이 캡슐화 정도가 그냥 0, 아무나 접근할 수 있으니까
private 멤버 변수에 접근할 수 있는 함수는 해당 클래스에 있거나
프렌드 클래스에 있다.
따라서 비멤버 비프렌드 함수의 사용은 캡슐화 정도를 증가 시킨다.
29. Item 23 : 멤버 함수보다는 비멤버 비프렌드 함수
비멤버 비프랜드 함수 예시
// 클래스와 비멤버 비 프랜드 함수를 같은 namespace공간에 놓음
namespace WebBrowserStuff {
class WebBrowser{…}; // 이 클래스에서 clearBrowser를 선언할 수도 있었다
void clearBrowser(WebBrowser& wb); // 비멤버 비프랜드 함수를 만들었다.
}
namespace는 여러 개의 소스 파일에 나뉘어 흩어질 수 있다.
따라서 다른 소스파일에서 WebBrowser에 관련된 내용을 만들고 같은
namespace로 묶을 수 있게 된다.
30. Item 23 : 멤버 함수보다는 비멤버 비프렌드 함수
표준 함수에 대한 내용인 std::이 namespace를 사용해 구성되어 있다.
이런 식의 구성은 새로운 비멤버 비프렌드 함수의 추가도 쉽게 할 수 있게 해준다.
32. Item 24 : 타입변환이 모든 매개 변수에 적용 돼야 할 때는 비멤버
비프렌드 함수를 쓰자
유리수 클래스를 가지고 이번 항목을 설명 할 것이다
class Rational {
public:
// explicit를 붙이지 않았다
// int에서 Rational로의 변환을 허용하기 위해
Rational(int numerator = 0, int denominator = 1);
…
// 곱셈 연산을 지원하는 operator
const Rational operator* (const Rational& rhs) const;
};
33. Item 24 : 타입변환이 모든 매개 변수에 적용 돼야 할 때는 비멤버
비프렌드 함수를 쓰자
Rational oneHalf = Rational(1,2);
Rational result = oneHalf * 2; // Ok! (1번)
result = 2* oneHalf; // error (2번)
왜 1번은 ok이가 2번은 error인가?
1번의 바뀐 표현 = 3번 result = oneHalf.operator*(2);
2번의 바뀐 표현 = 4번 result = 2.operator*(oneHalf);
1번의 경우 매개변수 리스트에 있는 2에 암묵적 변환이 일어난다
2번의 경우 매개변수 리스트에 들어있지 않기 때문에, Rational에 들어있는 멤버 함수를
사용하기 때문에 2를 Rational로 취급하지 않는다. 암묵적 변환이 일어나지 않는다.
34. Item 24 : 타입변환이 모든 매개 변수에 적용 돼야 할 때는 비멤버
비프렌드 함수를 쓰자
Rational result = oneHalf * 2; // Ok! (1번)
result = 2* oneHalf; // error (2번)
우리의 목표는 2번도 잘 되게 하는 것이다.
방법
* 연산을 할 때 lhs도 rhs도 암묵적 변환이 일어나게 하자.
1. 멤버 함수가 아닌 비멤버 비프렌드 함수를 사용하자
멤버 함수를 호출하는 객체는 암묵적 변환이 되지 않는다고 했다. 따라서 비멤버
비프렌드 함수를 사용하는 것이다.
2. 매개 변수 리스트에 lhs와 rhs를 넣자
이어서 …
35. Item 24 : 타입변환이 모든 매개 변수에 적용 돼야 할 때는 비멤버
비프렌드 함수를 쓰자
위의 내용을 적용한 새로운 operator *을 만들어 보자
비멤버 비프랜드 함수여야 하므로
class Rational {};
// Rational class 밖에서 선언 및 정의
const Rational operator* (const Rational& lhs, const Rational& rhs);
// 매개변수 리스트에 lhs, rhs 모두 넣음
36. Item 24 : 타입변환이 모든 매개 변수에 적용 돼야 할 때는 비멤버
비프렌드 함수를 쓰자
Rational oneHalf = Rational(1,2);
Rational result = oneHalf * 2; // Ok! (1번)
result = 2* oneHalf; // error (2번)
왜 1번은 ok이가 2번은 error인가?
1번 result = oneHalf.operator*(2);
2번 result = 2.operator*(oneHalf);
여기서 알 수 있는 것은 암시적 타입 변환이 매개변수 리스트에 들어 있는 객체에만
적용된다는 것입니다. 따라서 1번은 ok이고 2번은 암시적 변환이 되지 않아 2번인
것입니다.
38. Item 25 : 예외를 던지지 않는 swap에 대한 지원
namespace std{
template<typename T>
void swap(T& a, T& b)
{
T temp(a);
a = b;
b = temp;
}
}
이것이 std::swap(); 함수이다
에누리 없는 복사가 3회 일어난다.
39. Item 25 : 예외를 던지지 않는 swap에 대한 지원
class WidgetImpl{
public:
…
private:
int a, b, c;
std::vector<double> v;
}
// wiget에서 갖고 있을 내용
class Widget {
public:
Widget(const Widget& rhs);
Widget& operator=(const
Widget& rhs)
{
…
*pImpl = *(rhs.pImpl);
…
}
private:
WidgetImpl* pImpl;
}
앞으로 내용을 위한 pImpl 관련 내용
40. Item 25 : 예외를 던지지 않는 swap에 대한 지원
이전 슬라이드에서 본 예에서
Wiget 객체의 swap은 pImpl을 교환 하는 것이면 충분하다
그래서 Widget에 대한 swap 함수를 다음과 같이 표현 하였다.
namespace std{
// 아직 실행 안됨, 왜 안 되는지는 다음 슬라이드에서
// 템플릿 특수화, 어떤 타입에 대한 함수의 실행 방식을 미리 결정해 놓는 것
template<> void swap<Widget>(Widget& a, Widget& b)
{
swap(a.pImpl, b.pImpl);
}
}
41. Item 25 : 예외를 던지지 않는 swap에 대한 지원
만약 Widget과 WidgetImpl이 템플릿 클래스 였다면?
template<typename T> class WidgetImpl { … };
template<typename T> class Widget { … };
아래처럼 만들고 싶다.
// c++ 기준에 적합하지 않은 함수, 이유는 다음 슬라이드
namespace std{
template<typename T>
void swap<Widget<T>>(Widget<T>& a, Widget<T>& b)
{
a.swap(b);
}
}
42. Item 25 : 예외를 던지지 않는 swap에 대한 지원
적합 하지 않은 이유
함수 템플릿에서는 부분 특수화를 허용하지 않음 (클래스 템플릿은 허용)
void swap<Widget<T>> 부분특수화하는 부분
좀 바꿔서 해보려고 해도 안된다
// namespace std에는 템플릿 추가 불가, 따라서 아래 코드도 사용 불가
namespace std{
template<typename T>
void swap(Widget<T>& a, Widget<T>& b) // <Widget<T>> 부분 삭제
{
a.swap(b);
}
}
43. Item 25 : 예외를 던지지 않는 swap에 대한 지원
해결책 : namespace가 std가 아닌 곳에 해당 함수를 넣자
// stdWidgetStuff
namespace WidgetStuff{
template<typename T>
void swap(Widget<T>& a, Widget<T>& b) // <Widget<T>> 부분 삭제
{
a.swap(b);
}
}
44. Item 25 : 예외를 던지지 않는 swap에 대한 지원
그래서! 이 swap 함수를 사용할 때는 어떻게 하느냐?
swap이라는 이름을 가진 함수의 종류
1. std 일반형 버전
2. std 일반형을 특수화한 버전
3. T타입 전용버전
원하는 것
어떤 T타입 전용버전이 있으면 T타입 전용버전이 불리고, T타입 전용 버전이 없으면 std
일반형을 부르고 싶다.
45. Item 25 : 예외를 던지지 않는 swap에 대한 지원
template<typenameT>
void doSomething(T& objcet1, T object2)
{
using std::swap; // std::swap을 이 함수 안으로 가져옴
swap(object1, object2); // T타입 전용의 swap 함수를 호출
}
}
이게 되는 이유 => 다음 슬라이드
46. Item 25 : 예외를 던지지 않는 swap에 대한 지원
컴파일러가 이전 슬라이드의 swap 호출문을 만나면
1. 전역 유효범위 혹은 타입 T와 동일한 네임스페이스 안에 T 전용의 swap 함수가 있는
지 찾음
2. T전용 함수가 없다면 using std::swap 선언에 의해 std::swap 함수 사용
효율적인 swap 함수의 사용을 위해
이전 내용을 잘 공부해 두자