Item18: 올바르게 사용하기 쉽게 하고 잘못 사용하기 어렵게 만들기.
- 좋은 인터페이스는 올바르게 사용하기 쉽고, 잘못 사용하기 어렵다.
- 일관성을 유지하면 인터페이스 사용을 쉽게 만들 수 있다.
- 새로운 타입을 생성하고, 허용할 수 있는 값을 제한하며, const를 추가하는 등의 방법으로 클라이언트의 실수를 방지할 수 있다. (enum class 등)
- shared_ptr은 사용자 지정 deleter를 지원한다.
Item19: 클래스 설계를 타입 설계로 간주하기
C++에서 새로운 클래스를 정의하는 것은 새로운 타입을 정의하는 것과 같다.
좋은 타입은 자연스러운 구문(syntax), 직관적인 의미(semantics), 그리고 효율적인 구현(efficient implementation)이 필요하다.
- 새로운 타입의 객체는 어떻게 생성되고 소멸되는가?
- 새로운 타입의 객체는 객체 초기화와 객체 대입을 어 떻게 수행해야 하는가? (초기화와 대입은 다르다)
- 새로운 타입의 객체가 값으로 전달될 때 의미하는 것은 무엇인가?(복사 생성자의 동작 생각)
- 새로운 타입에서 허용되는 값의 범위는 무엇인가? (클래스의 불변성 정의)
- 새로운 타입이 상속 계층 구조에 포함되는가?
- 새로운 타입에서 어떤 타입 변환이 허용되어야 하는가? (명시적, 암시적)
- 새로운 타입에서 어떤 연산자 및 함수가 의미 있는가?
- 어떤 표준 함수는 제외해야 하는가?
- 누가 멤버 데이터에 접근할 수 있어야 하는가? (접근 제한자)
- 새로운 타입이 정말 필요한가??
Item20: pass-by-value 보다 pass-by-reference-to-const를 선호하기
기본적으로 사용하면 객체를 함수에 전달, 반환 시에 값에 의한 전달이 사용된다.
이러한 복사는 복사 생성자에 의해 수행되는데, 복사 비용이 높다면 비효율적일 수 있다.
따라서 const참조에 의한 전달(pass by reference to const)을 사용하면 불필요한 복사를 방지할 수 있다.
bool validateStudent(const Student& s); // 값이 아닌 `const` 참조로 전달
value를 참조로 전달하면 slicing problem(슬라이싱 문제)을 방지할 수 있다.
class Window {
public:
...
std::string name() const; // 윈도우의 이름 반환
virtual void display() const; // 윈도우 및 내용 그리기
};
class WindowWithScrollBars : public Window {
public:
...
virtual void display() const;
};
void printNameAndDisplay(Window w) // ❌ 잘못된 코드!
{
std::cout << w.name();
w.display();
}
WindowWithScrollBars wwsb;
printNameAndDisplay(wwsb);
위의 코드들을 보자.
base class의 display가 virtual함수이기 때문에
WindowWithScrollBars 타입을 선언하고, 이를 Window 타입에 담은 뒤 출력하면 동적 바인딩에 의해 derived class의 display가 실행되어야한다.
하지만 위의 printNameAndDisplay는 그렇지 않다.
함수의 파라미터인 Window w가 pass-by-value형식이기 때문에 Window가 복사되면서, WindowWithScrollBars의 데이터가 잘리게 된다(손실).
따라서
void printNameAndDisplay(const Window& w) // ✅ 올바른 코드 (슬라이싱 방지)
{
std::cout << w.name();
w.display();
}
pass by reference to const가 필요하다.
const는 파라미터로 넘어온 참조 안의 원본 데이터를 훼손시키지 않을 것이라는 보장이다.
일부는 pass-by-value가 더 유용할 수도 있다. (빌트인 타입, STL 반복자, 함수 객체)
아무튼 C++에서 객체를 전달할 때는 기본적으로 const 참조를 사용하자.
Item21: 객체를 반환해야 할 때 참조를 반환하려고 하지 마라.
Item20에서 배운 내용으로 인해 객체 return값을 참조로 하려고 시도할 수도 있다.
하지만 이는 매우 잘못된 접근 방식이다.
스택
const Rational& operator*(const Rational& lhs, const Rational& rhs) {
Rational result(lhs.n * rhs.n, lhs.d * rhs.d); // 로컬 변수 생성
return result; // ❌ 잘못된 코드: 로컬 변수의 참조를 반환
}
로컬 변수를 참조로 반환하면, 로컬 변수는 스코프를 빠져나가면 소멸하기 때문에 썩어가는 시체(rotting carcass)를 반환하는 것과 같다.
그래서 새로운 객체를 반환해야 하는 코드인 경우 참조를 반환해서는 안 된다.
힙
const Rational& operator*(const Rational& lhs, const Rational& rhs) {
Rational* result = new Rational(lhs.n * rhs.n, lhs.d * rhs.d); // 힙에 할당
return *result; // ❌ 잘못된 코드: 힙 객체의 참조 반환
}
힙에 할당하는 경우에는 new를 통해서 메모리를 할당하는데, 할당된 객체를 delete할 책임을 부여하기가 어렵다.
아무튼 절대 안된다고 함 (스마트 포인터라면?)
static?
thread safety보장이 되지 않는다.
또한 논리적 오류가 발생한다.
따라서 Nope
결론
operator*등 새로운 객체 반환이 필요한 경우 참조를 반환해서는 안된다.
올바른 방법은 객체를 값으로 반환하는 것이다.
Item22: 데이터 멤버를 private으로 선언하기
public 멤버는 완전히 노출되므로 변경이 어렵다.
protected 멤버는 파생 클래스에 사용되므로 변경이 어렵다.
private 멤버만이 완전한 캡슐화를 보장한다.
일관된 접근 방식, 세밀한 접근 제어, 클래스의 불변 조건, 구현 변경을 위해 멤버를 private로 선언하자.
Item23: 멤버 함수보다 비멤버 비프렌드(non-member non-friend) 함수를 선호하기.
객체지향의 핵심 원칙 중 하나는 캡슐화이다. 생각과 달리 비멤버 함수가 캡슐화를 더 제공할 수도 있다.
캡슐화가 수준이 높을수록, 외부에서 볼 수 있는 것이 줄어들고, 유연성이 늘어난다.
데이터에 접근하는 함수가 많을수록 캡슐화 수준은 낮아진다.
class WebBrowser {
public:
...
void clearCache();
void clearHistory();
void removeCookies();
...
};
이러한 클래스가 있을 때
class WebBrowser {
public:
...
void clearEverything(); // clearCache, clearHistory, removeCookies 호출
...
};
이것 보다
void clearBrowser(WebBrowser& wb) {
wb.clearCache();
wb.clearHistory();
wb.removeCookies();
}
이것이 더 좋다는 이야기이다.
멤버 함수는 클래스 내부에 있기 때문에 클래스의 private 데이터, private 함수, enum, typedef 등에 접근할 수 있지만,
비멤버 비프렌드 함수는, private 멤버에 전혀 접근할 수 없기 때문에 캡슐화 수준이 더 높다고 볼 수 있다.
namespace WebBrowserStuff {
class WebBrowser { ... };
void clearBrowser(WebBrowser& wb);
...
}
이러한 namespace를 사용하는 것도 코딩에 도움이 된다.
Item24: 모든 매개변수에 대한 형 변환이 적용되어야 할 경우 비멤버 함수를 선언하기.
어떤 함수에 들어가는 모든 파라미터가 형 변환이 필요하다면, 해당 함수를 비멤버로 만든다.
Item25: 예외를 던지지 않는 swap 지원을 고려하기.
기본적으로는 std::swap 을 사용하면 서로 바뀐다. (복사 생성자, 복사 대입 연산자 사용)
하지만 설계에 따라 swap이 느리게 작동할 수 있다.
namespace WidgetStuff {
... // 템플릿화된 WidgetImpl 등
template<typename T> // 이전과 동일, swap 멤버 함수 포함
class Widget { ... };
...
template<typename T> // 비멤버 swap 함수
void swap(Widget<T>& a, Widget<T>& b) {
a.swap(b);
}
}
네임스페이스를 사용하면, C++의 이름 탐색 규칙에 따라 네임스페이스의(Widget) swap을 먼저 쓰고, 다음에 std의 swap을 사용한다.
template<typename T>
void doSomething(T& obj1, T& obj2) {
using std::swap; // std::swap을 사용할 수 있도록 선언
...
swap(obj1, obj2); // T에 대한 가장 적절한 swap을 호출
...
}
이경우에도 타입별 swap이 존재하면 사용하고 없으면 std::swap을 사용하는 코드이다.
swap은 절대 예외가 발생하지 않도록 보장하기 위해 noexcept를 붙인다.
이렇게 하면 더 빠르게 동작한다. 다만 예외가 나지 않도록 면밀히 코드를 만들어야 할 것이다.
'C, C++ > Effective C++' 카테고리의 다른 글
| 6장 상속과 객체 지향 디자인 (0) | 2025.03.28 |
|---|---|
| 5장 구현 (0) | 2025.03.25 |
| 3장 리소스 관리 (0) | 2025.03.21 |
| 2장 생성자, 소멸자 그리고 할당 연산자 (2) (0) | 2025.03.17 |
| 2장 생성자, 소멸자 그리고 할당 연산자 (1) (0) | 2025.03.16 |