3. 자원 관리
13) new와 delete를 사용할 땐 반드시 형태를 맞추자
delete에 []가 붙었는지 아닌지로 배열 요소를 삭제해야하는지 아닌지 결정되기 때문에
new를 썼다면 delete를, new[]를 썼다면 꼭 delete[]를 써주도록 하자.
또 헤깔릴 일이 없도록 배열 타입은 typedef로 정의할 때 주의하자.
14) new로 생성한 객체를 스마트 포인터에 저장하는 코드는 별도의 한 문장으로 만들자
다음과 같은 코드는 컴파일되지 않는다.
void processWidget(std::shared_ptr<Widget> pw, int priority);
// 호출
processWidget(new Widget, getPriority());
shared_ptr의 생성자는 explicit 키워드로 선언되어 있기 때문이다.
반면, 아래 코드는 컴파일 된다.
processWidget(shared_ptr<Widget>(new Widget), getPriority());
그러나 컴파일러마다 연산 순서를 다르게 결정하며 new Widget과 shared_ptr에 저장되는 사이에 getPriority()를 호출하던 도중 예외가 발생할 수 있기 때문에 문제가 발생할 경우 디버깅도 굉장히 힘들며 좋지 못한 코드이다.
따라서 다음과 같이 스마트 포인터에 객체를 저장하는 부분을 별도의 문장 하나로 만든다.
std::shared_ptr<Widget> pw(new Widget);
processWidget(pw, getPriority());
이제 자원 누출 가능성이 없어진다.
4. 설계 및 선언
18) 인터페이스 설계는 제대로 쓰기 쉽게, 엉터리로 쓰기 어렵게 하자
a. 매개변수 타입 만들기
Date(int month, int day, int year);
위와 같은 생성자는 사용자가 실수할 가능성이 많다.
다음과 같이 매개변수 랩퍼 타입을 만들면 실수할 가능성을 줄일 수 있다.
struct Day {
explicit Day(int d) : val(d) {}
int val;
};
struct Month {
explicit Month(int d) : val(d) {}
int val;
};
struct Year {
explicit Year(int d) : val(d) {}
int val;
};
Date d(Month(3), Day(30), Year(1995));
만약 Month가 1-12만 가능하다는 제약을 걸고싶다면 다음과 같이 하는것도 좋다
class Month{
public:
static Month Jan() {return Month(1);}
...
private:
explicit Month(int m);
}
이 외에도 operator*의 반환 타입에 const를 붙이는 것과 같은 제약을 추가해 실수를 하기 힘들도록 만드는 것도 좋다.
b. 일관성있는 인터페이스 만들기
예를들어, 모든 STL 컨테이너는 size라는 멤버함수를 가지고 있다.
반면 자바의 경우엔 배열에 대해 length라는 프로퍼티를, String에 대해선 length라는 메서드를 부르고, List에 대해서는 size 메서드를 쓰게 되어있다.
이는 굉장한 혼란을 불러오기 때문에 좋지않다.
사용자 쪽에서 뭔가를 외워야 제대로 쓸 수 있는 인터페이스는 좋지 않다.
예를들어 자원을 반환하는 함수같은 경우 shared_ptr를 잘 활용하도록 하자
또한 해제 함수를 제공하는 것도 좋지는 않다. delete를 대신 사용하는 실수를 저지를 수 있기 때문이다.
단, shared_ptr을 사용하면 스레드 동기화 오버헤드를 일으키고, 원시 포인터의 두배 크기를 가진다.
하지만 이점이 더 크기에 잘 활용하자.
이 외에 기본제공 타입과의 호환성을 유지하고 타입에 대한 연산 제한하기, shared_ptr의 사용자 정의 삭제자를 활용하기 등이 있다.
19) 클래스 설계는 타입 설계와 똑같이 취급하자
- 새로 정의한 타입의 객체 생성 및 소멸은 어떻게?
직접 정의할 때 주의하자.
- 객체 초기화는 객체 대입과 어떻게 달라야 하는가?
초기화와 대입은 아예 다르므로 주의하자
- 새로운 타입으로 만든 객체가 값에 의한 전달이 되는 경우 어떻게 다룰 것인가?
값에 의한 전달은 복사 생성자에 의해 이루어진다.
- 새로운 타입이 가질 수 있는 값에 대한 제약은 어떻게 둘 것인가?
- 기존의 클래스 상속 그래프에 맞출 것인가?
상속을 결정했다면 설계가 큰 제약을 가지게 된다. 특히 가상 함수 여부가 결정된다.
- 어떤 종류의 타입 변환을 허용할 것인가?
가능한 암시적 변환, 명시적 변환에 대해 잘 제어해야 할 것이다.
- 어떤 연산자와 함수를 두어야 유의미할까?
- 표준 함수들 중 어떤것을 막아둘 것인가?
- 새로운 타입의 멤버에 대한 접근 권한을 어느 쪽에 줄 것인가?
- '선언되지 않은 인터페이스' 로 무엇을 둘 것인가?
- 새로 만들 타입이 얼마나 일반적인가?
- 정말 필요한 타입인가?
20) 값에 의한 전달보다 상수객체 참조자에 의한 전달 방식을 택하는 편이 대게는 낫다.
기본적으로 C++은 C에서부터 pass-by-value 방식을 사용한다.
이로인해 매개변수는 인자의 사본을 통해 초기화되며 어떤 함수를 호출한 쪽은 그 함수가 반환한 값의 사본을 돌려받는다.
그리고 이 사본을 만들어내는 곳이 복사 생성자이다.
그래서 여기서 고비용이 발생하기도 한다.
따라서 보통은 다음과 같이 상수객체에 대한 참조자로 전달하는 방식을 많이 택한다.
bool validateStudent(const Student& s);
이로인해 복사 손실 문제가 사라졌고, 매개변수는 변화로부터 안전한 보호를 받게 되었다.
그러나 int와 같은 기본 전달 타입일 때는 그냥 값으로 전달하는 편이 나을때도 있다.
그 점은 STL의 반복자와 함수 객체에 대해서도 마찬가지이다.
그러나 크기가 작은 사용자 정의 타입이라고 해서 무조건 값에의한 전달을 남발해서는 안된다.
컴파일러에 따라 사용자 정의 타입 객체는 레지스터에 저장하지 않는 경우도 있으니 말이다.
또한 개발 과정에서 해당 타입이 커질 가능성도 얼마든지 존재한다.
또, 상수객체 참조자는 함수 객체 타입에는 적절하지 않다
'C++ > Effective C++' 카테고리의 다른 글
Effective C++ (26) ~ (30) (0) | 2021.08.22 |
---|---|
Effective C++ (21) ~ (25) (0) | 2021.08.16 |
Effective C++ (11) ~ (15) (0) | 2021.08.12 |
Effective C++ (6) ~ (10) (0) | 2021.08.09 |
Effective C++ (1) ~ (5) (0) | 2021.07.31 |
댓글