Effective C++ Today I Learn (Day2)
Effective C++ Day2
객체를 사용하기 전에 반드시 그 객체를 초기화 하자!
- 초기화에 런타임 비용이 소모될 수 있는 상황이라면 값이 초기화 된다는 보장이 없습니다.
- 배열(C++의 C인 부분)은 각 원소가 확실히 초기화된다는 보장이 없습니다.
- STL의 vector(C++의 STL부분)는 초기화가 된다는 보장을 가지고 있습니다.
- 이렇기에 모든 객체를 사용하기 전에 항상 초기화 하는 것입니다.
대입(assignment)을 초기화(Initialization)와 헷갈리지 않는 것이 가장 중요합니다.
// '초기화'가 아닌 '대입'을 하고 있습니다.
DevRookie::DevRookie(const std::string& name, const std::string& address,
const std::list<PhoneNumber>& phones)
{
theName = name;
theAddress = address;
thePhones = phones;
numTimesConsulted = 0;
}
- 초기화는 생성자가 호출되기 전에 데이터 멤버의 기본 생성자가 호출되었습니다.
- 그 중 numTimesConsulted은 기본제공 타입의 데이터 멤버이기 때문에 생성자 안에서 대입되기 전에 초기화되리란 보장이 없습니다.
초기화 리스트(Initalizer List)를 사용하면 됩니다.
// 이제 생성자 본문에서 대입하는 것이 아닌 초기화를 하고 있습니다.
DevRookie::DevRookie(const std::string& name, const std::string& address,
const std::list<PhoneNumber>& phones)
: theName(name),
theAddress(address),
thePhones(phones),
numTimesConsulted(0)
{
// Do Nothing...
}
- 데이터 멤버에 사용자가 원하는 값을 주고 시작한다는 점에서는 똑같지만, 초기화 리스트를 사용하는 것이 더 효율적일 가능성이 큽니다.
- 대입을 사용한 버전에서는 기본 생성자를 호출해서 초기화를 미리 해 놓은 뒤, 생성자에서 곧바로 새로운 값을 대입하는 것입니다.
- 초기화 직후(데이터 멤버의 생성자) → 대입(복사 대입 연산자 호출)
- 초기화 리스트에서 쓰이는 인자는 데이터 멤버에 대한 생성자의 인자로 쓰이기 때문에 대입이 일어나지 않습니다.
- 초기화(복사 생성자 호출)
- 기본제공 타입의 객체는 초기화와 대입에 걸리는 비용의 차이가 없지만, 같이 초기화 리스트를 사용하는 것이 좋습니다.
- 대입을 사용한 버전에서는 기본 생성자를 호출해서 초기화를 미리 해 놓은 뒤, 생성자에서 곧바로 새로운 값을 대입하는 것입니다.
-
너무 과한 것 아닌가?
어떤 데이터 멤버가 멤버 초기화 리스트에 들어가지 않았고, 그 데이터 멤버의 타입이 사용자 정의 타입이면, 컴파일러가 자동으로 그들 멤버에 대해 기본 생성자를 호출하게 되어 있기 때문입니다.
하지만 초기화 리스트를 애용하는 센스를 보여줘야만, 어쩌다 리스트에서 멤버를 빼먹었을 때 어떤 멤버가 초기화되지 않을 수 있다는 사실을 끌고 가야 하는 부담이 없어지게 됩니다. (초기화가 반드시 이루어진다고 장담을 할 수 없기 때문입니다.)
초기화 리스트를 사용하는 것이 선택이 아닌 의무가 될 때도 있습니다. 바로 상수나 레퍼런스로 되어 있는 데이터 멤버의 경우입니다.
-
상수와 레퍼런스는 대입 자체가 불가능하기 때문입니다.
- C++에서 초기화가 변덕스럽지 않은 부분이 하나 있는데, 바로 객체를 구성하는 데이터의 초기화 순서입니다.
- 기본 클래스는 파생 클래스보다 먼저 초기화 됩니다.
- 클래스 데이터 멤버는 선언된 순서대로 초기화 됩니다.
- 멤버 초기화 리스트에 넣어진 순서가 다르더라도, 초기화 순서는 그대로입니다.(주의)
- 비지역 정적 객체의 초기화 순서는 개별 번역 단위에서 정해집니다.
- 정적 객체(static object)는 자신이 생성된 시점부터 프로그램이 끝날 때까지 살아 있는 객체를 일컫습니다.
- 스택 / 힙 기반 객체는 정적 객체가 될 수 없습니다.
- 전역 객체의 범주에 들어가는 것은..
- 전역 객체
- 네임스페이스 유효범위에서 정의된 객체
- 클래스 안에서 static으로 선언된 객체
- 함수 안에서 static으로 선언된 객체
- 파일 유효범위에서 static으로 정의된 객체
- 1, 2, 3, 5는 비지역 정적 객체(non-local static object)라고 합니다.
- 4는 지역 정적 객체(local static object)
- 번역 단위(translation unit)은 컴파일을 통해 하나의 목적 파일(object file)을 만드는 바탕이 되는 소스 코드를 일컫습니다.
- 기본적으로 소스 파일 하나가 되는데, 그 파일이 #include하는 파일들까지 합쳐서 하나의 번역 단위가 됩니다.
- 정적 객체(static object)는 자신이 생성된 시점부터 프로그램이 끝날 때까지 살아 있는 객체를 일컫습니다.
다시 정리하면, 서로 다른 번역 단위에서 정의된 비지역 정적 객체들의 상재거인 초기화 순서는 정해져 있지 않습니다.
class FileSystem
{
public:
std::size_t numDisks() const;
};
extern FileSystem tfs;
class Dircetory
{
public:
DirectoryCache(params);
};
Directory::Directory(params)
{
std::size_t disks = tfs.numDisks();
}
- 이 문제를 해결하기 위해 비지역 정적 객체를 지역 정적 객체로(함수를 호출하는 방식으로) 바꾸면 해결 가능합니다.
- 이 패턴이 바로 Singleton Pattern 입니다.
- 지역 정적 개체는 함수 호출 중 그 객체의 정의에 최초로 닿았을 때 초기화가 되도록 C++에서 보장해줍니다.
class FileSystem { ... }; FileSystem& tfs() { static FileSystem fs; return fs; } class Directory { ... }; Directory::Directory( params ) { ... std::size_t disks = tfs().numDisks(); ... } Directory& tempDir() { static Directory td; return td; }
정적 객체 자체를 직접 사용하지 않고, 그 객체에 대한 참조자를 반환하는 함수를 사용하도록 변경한 것입니다.
- 참조자 반환 함수는 구현이 단순하지만, 다중스레드 시스템에서는 동작에 장애가 생길 우려가 있습니다.
- 그렇기 때문에 다중스레드가 시작되기 전 미리 참조자 반환 함수를 미리 호출하도록 합니다.
- 그렇게 되면 초기화에 관계된 Race Condition을 예방할 수 있습니다.
Key Point
- 기본제공 타입의 객체는 직접 손으로 초기화합니다.
- 생성자 안에서 대입을 통한 데이터 입력 보다는 초기화 리스트를 즐겨 사용합니다. 추가로 초기화 리스트에 데이터를 나열할 때 각 데이터 멤버가 선언된 순서와 똑같이 하면 더 좋습니다.
- 여러 번역 단위에 있는 비지역 정적 객체들의 초기화 순서 문제는 피해서 설계해야 합니다. (비지역 정적 객체 → 지역 정적 객체)
Date: May 31, 2021
Tags:
Programming