시작하며
오랜만에 Effective C++를 다시 읽으려고 한다.
너무 옛날에 읽어서 기억이 안난다. 사실 그땐 이해도 거의 못했던 것 같다.
하루 다섯개씩 읽고 정리해서 11일동안 55개의 항목을 다 읽는게 목표다.
그다음엔 effective STL 읽어야지
1. C++에 왔으면 C++의 법을 따릅시다.
1) C++은 다양한 언어들의 조합이다.
C++는 C, 객체지향, 템플릿, STL의 조합이라고 생각하면 된다.
언제나 통용되는 최고의 규칙이란 없다.
C++의 어떤 부분을 어떤 상황에 사용하느냐에 따라 효과적인 선택지가 달라진다는 것을 명심하자.
2) #define을 사용해야 할 때에는 const, enum, inline을 떠올리자
#define은 전처리기로써 컴파일러에게는 전혀 보이지 않는다.
(a) 상수를 #define으로 정의한 경우, 컴파일러가 전혀 인지할 수 없으므로 디버깅도 어렵고, 코드의 양도 늘어날 수 있다.
보통 가장 간단한 해결책은 const 키워드를 이용해 상수(constant)로 변경하는 것이다.
이렇게 하면 컴파일러가 인지할 수 있으므로 디버깅이 수월해진다.
그러나 이때 두가지 경우를 조심해야 한다.
- constant pointer을 정의하는 경우 : 포인터를 const로 정의하는 경우엔, 가리키는 대상까지 const로 선언하는 것이 보통이다.
const char* const fruitName = "mango";
- 클래스에 상수 멤버를 정의하는 경우 : 이때 이 상수의 사본 개수가 한개를 넘지 못하게 하기 위해 static 멤버로 만들어야 한다. 보통은 정수류(integer, char, bool 등) 타입의 내부 상수는 다음과 같이 클래스 내에 정의 없이 선언만으로도 문제가 없다. ( 정의와 선언을 구분하자. )
class Player {
private:
static const int Turns = 10; // 정의 없이 선언되어 있다.
int scores[Turns]; // 상수를 사용하는 부분
}
그러나 가끔은 구현 파일에 정의를 제공해야 할 때도 있다.
// Player.cpp
const int Player::Turns; // 클래스 상수는 선언될 때 초기값이 주어진다.
위 방법으로 컴파일 되지 않을 때도 있다. 그럴땐 다음과 같이 초기값을 정의 시점에 주도록 변경하면 된다.
하지만 그렇게 되면 그 값을 해당 클래스를 컴파일하는 도중에 활용할 수 없다
(Player의 scores 배열 크기에 활용하는 것처럼)
그럴 땐 enum hack을 사용하자. (참고)
(b) 만약 #define을 활용해 매크로를 작성할 때에는 모든 인자를 괄호로 감싸주어야 한다. 그렇지 않으면 별 골치아픈 일이 다 생기기 때문이다.
#define CALL_WITH_MAX(a,b) f((a) > (b) ? (a) : (b))
이 때 인라인 함수에 대한 템플릿을 준비해 다양한 부작용을 해결할 수 있다.
template<typename T>
inline void callWithMax(const T& a, const T& b) // const &를 사용한 것에 주의
{
f(a > b ? a : b);
}
3) 최대한 const를 남발하자
(a) 포인터가 상수 vs 데이터가 상수 ?
둘 다 가능하다.
spiral rule을 활용하면 변수를 읽기 쉬워진다. (참고)
(b) 변경이 불가능한 객체를 가리키는 반복자는 const_iterator를 사용하면 됨.
(c) 함수 선언에 사용할 때 const 는 입력 매개변수, 출력 값, 멤버 함수에 붙을 수 있다.
- 함수 출력 값에 const를 붙여줄 경우 어이없는 실수 몇가지를 줄일 수 있다.
- const 매개변수는 사용할 수 있을 땐 거의 항상 사용할 수 있도록 유의하자.
- 멤버 함수에 붙은 const는 그 함수가 상수 객체에 대해 호출될 함수라는 것을 알리는 역할을 한다.
- 멤버 함수의 경우 const 키워드 유무만 다를 때 오버로딩이 가능하다.
- 일부 경우에 비트수준 상수성을 지키는 const 멤버함수 임에도 불구하고 논리적 상수성을 어기는 경우가 있다. 이 때 mutable 키워드를 사용해 논리적 상수성을 지키는 코드를 구현하도록 하자.
- 오버로딩된 const 멤버함수와 비 const 멤버함수가 비슷한 코드를 사용해 코드 중복을 줄이고자 할 경우에는 비상수 멤버함수가 const cast와 static cast를 이용해 상수버전의 멤버함수를 호출하도록 해보자. (단, 그 반대는 안된다)
4) 객체를 사용하기 전에 반드시 초기화하자
(a) C++의 객체는 그냥 선언했을 때 초기화되거나 초기화되지 않거나 둘 중 하나이다.
C로부터 넘어온 코드이며 초기화에 런타임 비용이 소모될 수 있는 상황에는 값이 초기화된다는 보장이 없다.
하지만 C++에서 생겨난 코드에서는 종종 초기화되기도 한다.
그러니까 정의되지 않은 동작을 막기위해 그냥 무조건 직접 초기화하는 습관을 들이자.
(b) 멤버 초기화 리스트(constructor member initializer list)를 적극적으로 활용하자.
- 객체들의 초기화 순서는 초기화 리스트에 적힌 순서가 아니라 클래스에 선언된 순서대로이다.
- 사용자 정의 타입이 아닌 기본제공타입도 모두 초기화 리스트에 넣어주는 쪽이 좋다.
- 상수나 참조자는 다른 선택지가 없다. 무조건 멤버 초기화 리스트를 사용해서 초기화하자.
- db나 파일을 읽어서 대입 초기화가 필요한 경우에 하나의 초기화용 private 함수를 선언해 활용할 수 있다.
- base 클래스가 derived 클래스보다 먼저 초기화된다.
(c) global static 객체는 초기화 순서가 불명이다.
정확히는 컴파일을 통해 object file을 만드는 재료가 되는 소스 코드들로 이루어진 단위를 translation unit 이라고 하는데, 별개의 translation unit에서 정의된 global static 객체들의 초기화 순서는 정해져 있지 않다.
이 때 global static 객체를 local static 객체로 변경해 이를 함수 호출로 접근할 수 있도록 변경해 해결할 수 있다. (Singleton 패턴 참고)
또한 서로의 초기화 순서때문에 문제가 발생하지 않도록 미리 순서를 맞춰두거나 설계를 잘 해야 한다.
2. 생성자, 소멸자 및 대입 연산자
5) C++가 은근슬쩍 만들고 호출하는 함수들에 주의하자
C++의 클래스 멤버함수 중에서 복사 생성자, 복사 대입 연산자, 소멸자는 사용자가 직접 선언하지 않을 경우 필요할 때 자동으로 public inline으로 기본형이 삽입된다.
컴파일러가 만들어주는 복사생성자와 복사대입연산자는 destination의 각 멤버함수의 생성자에 source의 멤버함수를 넘기는 식으로 이루어진다.
그런데 참조자나 상수 객체를 멤버로 가질 경우 이러한 방식으로 복사대입연산자가 구현되면 적법하지 않으므로, 이 때엔 직접 복사 대입 연산자를 정의해야 한다.
'C++ > Effective C++' 카테고리의 다른 글
Effective C++ (26) ~ (30) (0) | 2021.08.22 |
---|---|
Effective C++ (21) ~ (25) (0) | 2021.08.16 |
Effective C++ (16) ~ (20) (0) | 2021.08.14 |
Effective C++ (11) ~ (15) (0) | 2021.08.12 |
Effective C++ (6) ~ (10) (0) | 2021.08.09 |
댓글