Item 26 : Postpone variable definitions as long as possible
변수의 정의를 변수를 바로 사용해야할 때 직전에 해야할 뿐만 아니라, 변수를 위한 초기화 값을 가지기 전까지 변수 정의를 연기해야 한다.
이렇게 함으로써, 불필요한 객체에 대한 생성자와 소멸자가 호출되는 것을 피하고, 의미가 명확한 상황에서 변수를 초기화하여 변수의 목적을 문서화하는데 도움이 된다.
loop를 사용하는 경우, 변수를 loop문 안에 정의하거나, 밖에 정의할 수 있다.
할당이 생성자-소멸자보다 비용이 적고, 코드의 성능에 민감한 부분을 처리하지 않는 한 기본적으로 변수를 안에 정의해야한다.
Item 27 : Minimize casting
캐스팅은 type system을 파괴하며 모든 종류의 문제를 야기할 수 있다.
C에서 제공하는 old-style의 cast는 다음과 같다.
(T) expression // cast expression to be of type T
T(expression) // cast expression to be of type T
C++는 4개의 새로운 cast 형태를 제공한다(new-style또는 C++-style cast라고 부름).
각각은 다른 목적을 가진다.
1. const_cast<T>(expression)
객체의 const를 없애는데 사용된다(const to non-const).
2. dynamic_cast<T>(expression)
safe downcasting을 수행하는데 주로 사용된다.
즉, 객체를 상속 계층의 특정한 타입으로 결정한다.
이것은 old-style syntax를 사용해 수행할 수 없는 유일한 cast이며 상당한 런타임 비용이 필요할 수도 있다.
3. reinterpret_cast<T>(expression)
구현결과에 의존적인 low-level의 cast이다.
4. static_cast<T>(expression)
implicit conversion(암시적 변환)을 강제하는데 사용된다.
많은 conversion들을 거꾸로 수행할때도 사용한다.
ex) void* to int*, base* to derived*, non-const to const
old-style의 cast도 합법적이지만, 새로운 형태의 캐스트가 선호된다.
새로운 형태의 캐스팅이 더 식별하기 쉽고, 캐스트 사용 목적을 더 좁혀서 지정할 수 있기 때문이다.
old-style의 캐스트는 함수에 객체를 전달할 때 explicit constructor를 사용하는 경우에만 사용한다.
static_cast도 이와 같은 기능을 수행할 수 있다.
doSomeWork(Widget(15)); // function-style cast
doSomeWork(static_cast<Widget>(15)); // c++-style cast
많은 프로그래머들은 캐스트가 컴파일러에게 한 타입을 다른 타입으로 다루라고 말하는 줄 알지만, 이는 오해다.
어떤 종류의 타입 변환이든 런타임에 실행되는 코드에서 발생한다.
class Base{};
class Derived : public Base{};
Derived d;
Base* pb = &d; // implicitly convert Derived* to Base*
위 코드에서, derived class의 객체를 가리키는 base class pointer를 생성했다.
하지만 때때로 두 포인터 값은 같지 않다.
runtime(런타임)에 offset이 Derived* pointer에 적용되어 Base* pointer의 값을 구한다.
즉, 한 객체가 한 개 이상의 주소를 가질 수 있다.
이것은 C++에서만 발생하는데, multiple inheritance가 사용되고 거의 항상 발생하는 경우 발생한다.
하지만 single inheritance인 경우에도 발생할 수 있다.
따라서, 캐스트를 사용하지 않아야 한다.
캐스트의 흥미로운 점은 무언가를 올바르게 쓰는것이 쉽다는 것이지만 이는 틀렸다.
예를들어, derived class의 virtual member 함수가 그것의 base class의 부분을 먼저 실행시키도록 구현해야 하는 경우를 고려해보자.
class Window{
public:
virtual void onResize(){...}
};
class SpecialWindow: public Window{
public:
virtual void onResize(){
static_cast<Window>(*this).onResize();
}
};
코드는 *this를 Window로 캐스트하며 onResize를 호출하면 Window::onResize가 호출된다.
그러나 이는 현재 객체에 대한 함수를 호출하는 것이 아니다!
대신에 cast는 새로운 *this의 base class 부분에 대한 temporary 복사본을 생성하며, 따라서 onResize는 복사본에서 호출된다.
이에 대한 해결방법은 캐스트를 제거하고 다른 방법으로 대체하는 것이다.
위 코드에서 실제로 원하는 것은 현재 객체에 대한 onResize의 base class version을 호출하는 것이다.
virtual void onResize(){
Window::onResize();
}
만약 캐스트를 하기를 원한다면, 잘못된 방법으로 문제에 접근한 것이다.
이것은 특히 dynamic_cast를 사용하고 싶을때 적용된다.
dynamic_cast는 매우 느리다.
따라서, dynamic cast를 사용하는 대신, 타입 안전성을 가진 container를 사용하거나, virtual function을 계층 위로 옮기는 방법을 사용한다.
잘 짜여진 코드는 cast가 거의 없다.
하지만 일반적으로 cast를 모두 제거하는 것은 비효율적이다.
int를 double로 변환하는 것은 cast가 엄격하게 필요하지 않을지라도 타당한 사용방법이다.
정리하자면, 실용적이라도 cast 사용을 피해라.
특히 성능에 민감한 코드에서 dynamic_cast의 사용을 피해야 한다.
만약, casting이 필요하다면, cast-free 대안을 개발을 시도해야 한다.
casting이 필요하다면, 함수 안에 숨겨야 한다.
클라이언트는 코드에 cast를 사용하는 대신, 함수를 호출할 수 있다.
old-stype의 캐스팅보다는 C++ 스타일의 캐스팅을 사용해라.
더 보기 쉽고, 무엇을 수행할지 명확하다.
Item 28 : Avoid returning "handles" to object internals
클래스 Rectangle에 대한 멤버 함수 upperLeft와 lowerRight를 살펴보자.
Point& upperLeft() const{ return pData->ulhc; }
Point& lowerRight() const{ return pData->lrhc; }
이 두 함수는 Rectangle의 point가 무엇인지를 알려주기만 하고, Rectangle을 수정하지 못하도록 디자인되었기 때문에 const로 선언되었다.
그러나, private internal data에 대한 reference를 리턴하기때문에, reference를 사용하여 internal data를 수정할 수 있다.
Point coord1(0,0);
Point coord2(100,100);
const Rectangle rec(coord1, coord2);
rec.upperLeft().setX(50);
따라서 위와 같은 코드가 가능하다.
여기서 알 수 있는 것은, 첫번째, 데이터 멤버는 참조를 반환하는 가장 접근하기 쉬운 함수만큼만 캡슐화된다.
두번째, 객체와 연관된 데이터에 대한 refernce를 리턴하는 const member function을 호출하여 그 데이터를 변경할 수 있다.
Reference뿐만 아니라 pointer, iterator도 다음과 같은 문제가 발생할 수 있다.
Refernce, pointer, iterator는 handle이라고 하며 객체의 internal data에 대한 handle을 반환하는 것은 객체의 캡슐화를 손상시킬 위험이 있다.
따라서, 멤버 함수가 handle을 반환하지 않도록 해야한다.
반환한 handle을 수정하지 못하도록 다음과 같이 코드를 변경할 수 있다.
const Point& upperLeft() const { return pData->ulhc; }
const Point& lowerRight() const { return pData->lrhc; }
그러나, 이 접근방법은 클라이언트가 Point를 읽을 수는 있지만, 쓸 수는 없다.
따라서, 캡슐화 문제가 발생한다.
오직 read access만 허용되며, write access는 금지된다.
또한, 여전히 handle을 반환하기 때문에, dangling handle문제를 발생시킬 수 있다.
어떤 객체의 internal들에 대한 handle(reference, pointer, or iterator)을 반환하는 것을 피하자.
이것은 encapsulation을 증가시키고, const 멤버 함수가 const로 동작할 수 있게 하며 dangling handle이 발생하는 경우를 줄일 수 있다.
'C++ > Effective C++' 카테고리의 다른 글
[Effective C++] Chapter 6: Inheritance and Object-Oriented Design(1) (0) | 2022.07.06 |
---|---|
[Effective C++] Chapter 5: Implementations(2) (0) | 2022.07.06 |
[Effective C++] Chapter 4: Design and Declarations(2) (0) | 2022.07.05 |
[Effective C++] Chapter 4: Design and Declarations(1) (0) | 2022.07.02 |
[Effective C++] Chapter 3: Resource Management (0) | 2022.07.01 |