Item 18 인터페이스 설계는 제대로 쓰기엔 쉽게, 엉터리로 쓰기엔 어렵게 하자.

함수, 클래스, 템플릿 등이 인터페이스이다.

‘제대로 쓰기엔 쉽고 엉터리로 쓰기엔 어려운’ 인터페이스 개발은 우선 사용자가 저지를 만한 실수의 종류를 머리에 암두해야된다.

  1. 좋은 인터페이스는 제대로 쓰기에 쉽고, 엉터리로 쓰기 어렵다. 인터페이스를 만들때는 이 특성을 지닐수 있도록 해야된다.

  2. 인터페이스의 올바른 사용으로 이끄는 방법은 인터페이스 사이의 일관성 잡기, 기본제공 타입과의 동작 호환성 유지하기가 있다.

  3. 사용자의 실수를 방지하는 방법으로는 새로운 타입 만들기, 타입에 대한 연산 제한하기, 객체의 값에 대해 제약 걸기, 자원 관리 작업을 사용자에게 전가하지 않기가 있음.

  4. shared_ptr은 사용자 정의 삭제자가 지원된다. 이 특징 덕에, shared_ptr은 교차 DLL문제를 막아주며, 뮤텍스 등을 자동으로 잠금 해체하는데 쓸 수있다.

    교차 DLL : 생성 시에 A DLL의 new를 썼는데 지울 때 A` DLL의 delete로 지우는 경우 인터페이스 설계

Item 19 클래스 설계는 타입 설계와 똑같이 취급하자

좋은 타입은 문법이 자연스럽고, 의미구조(Sematic)가 직관적이고, 효율적 구현이 한가지 이상이 가능.

  • 새로 정의된 타입의 객체 생성 및 소멸이 이루어지는 과정

    • 메모리 할당과 생성자, 소멸자의 설계
  • 객체 초기화는 객체 대입과 어떻게 다른가

    • 생성자와 대입 연산자의 동작 및 둘 사이의 차이점을 결정
  • 새로운 타입으로 만든 객체가 값에 의해 전될되는 경우에 어떤 의미를 줄것인가

    • 값에 의한 전달이 일어난다는 것은 복사 생성자를 의미한다.
  • 새로운 타입이 가질수 있는 적법한 값에 대한 제약은?

    • 클래스의 데이터 맴버의 몇 가지 조합 값은 반드시 유효해야됨. -> 불변속성
  • 기존의 클래스 상속에 맞출것인가

    • 상속을 시키면 이들 클래스에 의해 제약을 받게 된다. 특히 맴버 함수의 가상여부가 크다. 상속받게 만들었다면, 이에 따라 맴버 함수의 가상 함수 여부가 결정된다.
  • 어떤 종류의 타입 변환을 허용할것인가

    • T1 타입과 T2 타입의 객체로 암시적으로 호환되게 만들고 싶다면, 타입변환 함수를 만들고, 인자 하나로 호출될수 있는 비명시 호출 생성자를 T2에 넣어야 된다.
    • 명시적 타입 변환만 허용하고 싶다면 해당 변환을 맡는 별도 이름의 함수를 만들고, 타입 변환 연산자 혹은 비명시 호출 생성자는 만들지 말아야 된다.
  • 어떤 연산자와 함수를 두어야 의미가 있는가

  • 표준 함수들 어떤 것을 허용하지 말아야 하는가

    • private로 선언해야 되는 함수가 여기에 해당
  • 새로운 타입의 맴버에 접근권한을 어느쪽에 줄것인가

    • 맴버를 public, protected, private 영역에 둘것인가를 결정한다.
  • 선언되지 않은 인터페이스로 무엇을 둘것인가

    • 내가 만들 타입이 제공할 보장이 어떤 종류에 대한 질문, 보장할수 있는 부분은 수행 성능, 예외성 그리고 자원 사용이다.
  • 새로 만드는 타입은 얼마나 일반적인가

    • 매우 일반적이라면 클래스가 아니라 템플릿을 만들어야 될수도 있다.
  • 정말로 필요한 타입인가

    • 기존 클래스에 대한 기능 몇개가 아쉬워서 파생 클래스를 만들고 있다면, 차라리 간단하게 비맴버 함수 템플릿 몇개 더 정하는 게 낫다.

Item 20 값에 의한 전달보다, 상수객체 참조자에 의한 전달이 낫다.

  1. 기본제공 타입 및 STL 반복자, 그리고 함수 객체 타입에는 맞지 않다. 이들에 대해서는 값에 의한 전달이 적절하다.

Item 21 함수에서 객체를 반환해야 할 경우에 참조자를 반환하려고 들지 말자.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
const Rational& operator*(const Rational& lhs, const Rational& rhs)
{
Rational result(lhs.n * rhs.n, rhs.d * lhs.d); // result은 이 함수를 넘어가면 사라진다.
return result;
}
// 이 함수는 온전한 Rational에 참조객체를 리턴하지 않는다.

const Rational& operator*(const Rational& lhs, const Rational& rhs)
{
Rational *result = new Rational(lhs.n * rhs.n, rhs.d * lhs.d); // 여전히 생성자가 호출중이다.
return *result;
}
// 이 객체를 누군가가 delete 해주어야 하는데 누가하는가?

Rational w, x, y, z;
w = x * y * z; // 일때 이에 해당하는 모든 delete 호출은 누가?

const Rational operator*(const Rational& lhs, const Rational& rhs)
{
return Rational(lhs.n * rhs.n, rhs.d * lhs.d);
}

Item 22 데이터 맴버가 선언될 곳은 private 영역임을 명심하자

왜 public을 쓰면 안될까?

public이 아니라면 맴버 함수뿐만이라면, 사용자가 해당 클래스의 맴버에 접근하고 싶을때 고생이 줄게된다 그리고 구현상의 융통성을 전부 누릴수 있게 된다. protected도 사실 파생 클래스가 많을때에는 효율적이지 않다. public보다 더 많이 보호 받고있는 것이 절대로 아니다.

Item 23 맴버 함수보다는 비맴버 비프렌드 함수와 더 까가까워지자

  1. 프렌드 함수는 private 맴버에 대한 접근권한이 해당 클래스의 맴버 함수가 가진 접근권한과 똑같기 때문에, 캡슐화에 대한 영향이 같다.
  2. 캡슐화에 대한 이야기 때문에 함수는 어떤 클래스의 비 맴버가 되어야 된다라는 의미가 아니다. private 맴버의 캡슐화에 영향을 주지 않는 점이 더 중요하다.

클래스와 함수를 같은 네임스페이스 안으로 넣자. 거의 모든 사용자가 써야 되는 비맴버 함수들을 넣으면 된다.

이로써 캡슐화 정도가 높아지고, 패키징 유연성도 커지며, 기능적인 확장성도 늘어난다.

1
2
3
4
5
6
namespace Stuff
{
class P { .... };

void OpenP(P& wb);
}

Item 24 타입 변환이 모든 매개변수에 적용되어야 한다면 비맴버 함수를 선언하자

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
32
33
34
35
class Rational
{
public:
const Rational operator*(const Rational& rhs) const;
}

Rational oneHalf(1, 2);
Rational oneEight(1, 8);
Rational result;

result = 2 * oneHalf; // 에러

// result = 2.operator*(oneHalf); 형태로 호출하기 때문에 에러가 난다.
// 암시적 타입 변환(implicit type conversion)에 이유가 있다.
// 컴파일러는 이렇게 처리한다.
const Rational Temp(2); // 임시 객체 생성
result = onehalf * temp;

// 명시호출(explicit)로 선언되지 않은 생성자가 있기 때문에 이렇게 동작한다.
// 만약 explicit로 선언된 생성자라면 둘다 동작하지 않는다.

// 암시적 타입 변환에 대해 매개변수가 먹힐려면 매개변수 리스트에 들어 있어야만 한다.

class Rational
{
....
};

const Rational operator*(const Rational& lsh, const Rational& rhs)
{

}

// operator* 함수를 프랜드로 선언해도 될까?
// 만약 해당 클래스의 public 요소만으로 계산이 된다면 아니오, 필요하다면 yes다.

어떤 함수에 들어가는 모든 매개변수 (this 포인터가 가르키는 객체도 포함)에 대해 타입 변환을 해줄 필요가 있다면, 그 함수는 비맴버여만 한다.

Item 25 예외를 던지지 않은 Swap에 대한 지원도 생각하자.

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
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
class Widget
{
public:
void swap(Widget& other)
{
std::swap;

swap(p, other.p);
}
}

namespace std
{
template<> // 템플릿 특수화 => std::swap의 완전 템플릿 특수화 함수라는 것을 알려준다.
void swap<Widget>(Widget& a, Widget& b)
{
a.swap(b);
}
}

// 만약 클래스템플릿으로 만들어져 있어서, WidgetImp1에 저장된 데이터 타입을 매개변수로 바꿀수 있다면?

template<typename T>
class WidgetImpl {};
template<typename T>
class Widget {};

namespace std
{
template<typename T>
void swap<Widget<T>>(Widget<T> &a, Widget<T> &b) // 에러
{ a.swap(p);}
}

// 함수 템플릿을 부분적으로 특수화 해달라고 요청하였다.
// C++은 클래스 템플릿에 대해서는 부분 특수화를 허용하지만, 함수 템플릿에 대해서는 허용하지 않도록 되어 있음.
// 함수 템플릿을 부분적으로 특수화하고자 한다면 오버로드 버전을 하나 추가한다.

namespace std
{
template<typename T>
void swap(Widget<T> &a, Widget<T> &b)
{ a.swap(b); } // std 네임스페이스에 대해서 std내의 템플릿에 대한 완전 특수화는 OK, 새로운 템플릿을 추가하는것은 OK가 아니다.
}

// 즉 위에서는 새로운 네임스페이스 안에 동작하도록 만들어야된다.
namespace WidgetStuff
{
template<typename T>
void swap(Widget<T> &a, Widget<T> &b)
{ a.swap(b); }
}

// 어떤 코드가 두 Widget 객체에 대해 Swap을 호출하더라도 컴파일러는 C++의 이름탐색 규칙에 의해 WidgetStuff 네임스페이스 안에서 Widget 특수화 버전을 찾아낸다.

template<typename T>
void doSomething(T& obj1, T& obj2)
{
using std::swap;

swap(obj1, obj2);
}
  1. 표준에서 제공하는 swap이 클래스 및 클래스 템플릿에 대해 납득할 만한 효율을 보이면 아무것도 하지말자.
  2. 표준 swap 의 효율이 기대한 만큼 충분하지 않다면 (클래스와 클래스 템플릿이 pimpl 관용구와 비슷하게 만들어져 있는 경우라면) 다음과 같이 한다.
    1. 원하는 타입으로 만들어진 두 객체의 값을 빨리 맞바꾸는 함수를 swap이라는 이름으로 만들고, 이것을 public 맴버 함수로 둔다. 이 함수는 절대 예외를 던지지 않는다.
    2. 클래스 혹은 템플릿이 들어있는 네임스페이스와 같은 네임스페이스에 swap을 만든다. 그리고 1번에서 만든 swap함수를 이 비맴버 함수가 호출하게 한다.
    3. 새로운 클래스를 만들고 있다면 std::swap의 특수화 버전을 준비해둔다.
  3. 사용자 입장에서 swap을 호출할때, swap을 호출하는 함수가 std::swap을 볼수 있도록 using선언을 포함해준다.

pimpl : pointer to implementation 이라는 뜻으로, std::swap은 포인터를 모르고 객체를 복사한다음, 옮기게 된다. 그럴경우 swap 연산이 느려진다.