항목 26 : 변수 생성 타이밍은 신중히하자.
클래스 객체 같은 경우 변수의 생성은 기본적으로 큰 비용을 따른다. 생성자의 호출 및 이후 소멸자의 호출을 하기 때문이다. 만약 어떤 함수에서 변수를 생성할 땐 필요한 순간까지 기다렸다가 필요한 순간에 생성하는 것이 좋다. 괜히 함수 호출 초반에 변수를 생성하면 예외 처리등에 의해 해당 변수를 사용하기전에 함수가 리턴되어 끝나버릴 수도 있기 때문이다. 이경우 변수는 사용되지도 않았으면서 생성자 및 소멸자에 대한 비용을 유발하기에 성능에 좋지않다. 디자인의 측면에서도 변수를 실제 사용전 생성하면 생성하는 변수의 용도를 알기 쉬워 좋다.
추가로 변수 생성 후 대입보다는 해당 초기화 값으로 바로 생성할 수 있으면 그렇게 하는 것이 당연히 더 성능이 좋다.마지막으로 반복문에서 사용하는 변수의 경우 반복문 밖에서 생성하고 안에서 새값을 대입하여 사용하는 것과 반복문 안에서 값을 바로 초기화 하며 생성하는 두가지의 방법이 있는데 각자 장단점이 있다. 전자는 1번의 생성자와 1번의 소멸자, n번의 대입이 발생한다. 생성자+소멸자에 비해 대입의 연산이 빠르면 유용하다. 그에 비해 후자는 n번의 생성자와 n번의 소멸자를 호출한다. 하지만 변수의 사용 가능 범위가 반복문 안으로 한정된다는 점에서 유지보수에 유용하다.
Widget w;
for (int i = 0; i < n; ++i)
{
w = (some value dependent on i);
...
}
for (int i = 0; i < n; ++i)
{
Widget w(some value dependent on i);
...
}
항목 27 : 캐스팅은 신중히 사용하자.
캐스팅은 코드의 구현을 편하게 만들 수도 있지만 예상치 못한 오류를 정말 많이 발생시키는 양날의 검이다. 따라서 캐스팅은 정말 신중히 사용해야한다.
캐스팅 종류
우선 캐스팅의 종류를 나누어보자.
c 캐스팅
(T) expression 또는 T(expression)
단순하지만 제약이 거의 없어 잘못된 캐스팅을 잘 잡지못하며 세분화된 용도가 없기에 컴파일러도 잘못된 사용을 잘 검출해내지 못한다. 왠만해선 c++캐스팅을 쓰는 것이 좋다.
c++ 캐스팅
const_cast<T>(expression)
상수성을 없애는데 사용
dynamic_cast<T>(expression)
런타임에 캐스팅 에러를 체크하는 캐스팅, static_cast에 비해 컴파일 시 에러 검출이 약하지만 런타임 시 더 강력한 에러 검출을 해준다. 정상적인 상속 계층구조간 타입변환인지 검출해줄 수 있기에 자손 타입을 품고 있는 부모 타입의 변수를 자손 타입 변수로 다운 캐스팅 할때 주로 사용한다. 정상적이지 않은 캐스팅일 시 에러대신 0을 리턴한다. 대신 비용이 크다는 단점이 있다. 따라서 성능을 생각한다면 최대한 사용을 피해야 한다.
reinterpret_cast<T>(expression)
int -> 포인터 등의 하부수준 캐스팅을 처리하는 캐스팅이다. 구현환경에 의존적이며 애플리케이션 수준에서는 거의 쓰이지 않는다.
static_cast<T>(expression)
컴파일 시 캐스팅 에러를 체크하는 캐스팅, 컴파일 시 캐스팅 에러를 잡아주지만 dynamic_cast에 비해 세밀한 에러를 잡아주지 못한다. 컴파일에서 잡지 못한 정상적이지 않은 캐스팅시 미정의 동작이 발생할 수 있다. 대신 성능이 좋다.
캐스팅의 의미
캐스팅은 단순히 변수의 메모리 데이터를 다른 타입으로써 바라보는 것으로만 처리되지않는다.
int를 double로 바꾸는 경우를 생각해보자. int와 double의 구조는 완전히 다르다. 이경우 int의 논리적 값을 가지는 double이 따로 생성되는 등의 코드가 생성될 것이다. 단순히 기존 int의 메모리를 double로써 바라보는 것과 완전히 다르다.
다음은 A라는 객체의 포인터와 A의 클래스의 자식 객체의 포인터인 B가 있다고 해보자.
A = B; 를 호출하면 B의 포인터가 부모 타입의 포인터로 변환되어 A에 들어간다. 이때 A와 B가 가르키는 메모리 주소가 다를 수도 있다. B의 클래스가 추가적인 멤버를 정의했다면 부모 객체와 자식 객체의 메모리 배치 구조는 다를 것이다. 이때 같은 객체를 가르키더라도 부모 타입 포인터로써 부모 객체의 메모리 구조를 고려하여 포인터가 가르켜야하는 지점과 자식 타입 포인터로써 자식 객체의 메모리 구조를 고려하여 포인터가 가르켜야하는 지점은 다를 수도 있다. 따라서 A와 B의 물리적 비트가 다를 수 있다는 것이다. 그리고 이는 구현 환경마다 다 다르게 나타나기에 더욱 예측하기 어렵다.
마지막으로 T(expression)이라는 구문을 생각해보자. 이는 expression의 T로의 캐스팅이자. T타입의 expression타입 하나를 받는 생성자를 호출하는 구문이다. 즉 여기서 캐스팅이나 생성자 호출이나 같은 구문으로 표현되고 있다는 것이다. 이 시점에선 expression을 T타입으로 캐스팅한다는 것이 적절한 생성자를 호출하여 T타입 객체를 만드는 것과 같다는 것이다. 캐스팅이 이렇듯 단순히 기존 메모리를 다르게 해석한다는 것 이상의 의미를 가질 수 있다.따라서 캐스팅을 사용할 땐 이러한 점을 꼭 유의해서 사용하자.
객체 스스로 캐스팅
자식 클래스에서 조상 클래스의 가상함수를 오버라이드 하여 사용할 때 조상 클래스의 가상 함수 버전을 먼저 호출하고 자신의 구현을 추가하는 방식을 많이들 사용한다. 이때 자신의 조상 버전 함수를 호출하고자 할 때 *this를 캐스팅해서 호출하는 행위는 절대 해서는 안된다. 예를 들어 static_cast<A>(*this).vf();는 자신을 레퍼런스를 A타입으로 변환하여 vf를 호출하는 것이 아닌 자신을 이용하여 새로운 A타입의 객체를 생성하고 그 객체의 vf를 호출하는 것이다. 즉 의미 없는 짓이 된다. 따라서 반드시 A::vf(); 이런식으로 호출하자.
항목 28 : 클래스 내부 데이터 핸들을 주는 코드는 삼가자.
클래스의 멤버 변수에 대한 참조, 포인터, 반복자등의 핸들을 외부로 주는 코드는 삼가는 것이 좋다. 이는 물리적 멤버 뿐 아니라 멤버가 가르키거나 소유한 논리적 멤버들도 포함하는 말이다. 만약 참조를 완전히 넘겨준다면 외부에서 해당 멤버의 참조를 받아 수정이 가능해진다. 따라서 멤버가 private라 할지라도 참조를 주는 함수가 public이라면 public수준으로 해당 멤버에 접근할 수 있는 것이다. 따라서 캡슐화가 무너진다.
const 참조 반환도 삼가자
만약 const 참조를 넘겨준다 하더라도 문제는 남아있다. const 참조를 넘겨주면 외부에서 해당 참조로 값을 수정할 순 없어 수정에 대한 캡슐화는 방어될 것이고 읽기에 대한 캡슐화는 보통 완화하는 경우가 많으니 캡슐화는 크게 문제가 없게 된다. 하지만 외부에서 const 참조를 받고 가지고 있는데 만약 해당 멤버를 가진 객체가 소멸되어 버린다면 해당 참조는 소멸된 데이터를 가르키게 될 것이다. 이러한 문제가 발생할 수 있기에 내부 멤버에 대한 const 참조도 반환을 삼가야 된다. 대신 값 전달등을 사용하는 것이 좋다.
참고서적
Effective C++
'프로그래밍 언어 > Effective C++' 카테고리의 다른 글
[Effective C++]항목 32~34: 객체 지향 설계 1 (0) | 2023.05.15 |
---|---|
[Effective C++]항목 29~31: 구현 2 (0) | 2023.04.27 |
[Effective C++]항목 23~25 : 클래스에 대한 비멤버 함수 (0) | 2023.04.10 |
[Effective C++]항목 18~22 : 클래스 설계 기초 (0) | 2023.04.10 |
[Effective C++]항목 13~17 : 자원 관리 (0) | 2023.03.29 |