본문 바로가기

C, C++/Effective C++

[Effective C++] 5장 구현

개요

https://product.kyobobook.co.kr/detail/S000001962302

 

Effective C++ | 스콧 마이어스 - 교보문고

Effective C++ | [Effective C++]은 C++ 프로그래밍과 설계 기술을 향상시켜 주는 55가지 명쾌한 테크닉을 모은 책이다.

product.kyobobook.co.kr

이번 장에서는 구현 시 주의할 사항들을 담고 있습니다.

https://basaeng.tistory.com/22

 

5장 구현

Item26: 가능한 한 변수를 늦게 정의하라.제어 흐름이 변수 정의에 도달하면 생성 비용이 발생하고, 변수가 범위를 벗어날 때 소멸 비용이 발생한다.변수가 사용되지 않으면 낭비가 발생되기 때문

basaeng.tistory.com

 


Item26: 가능한 한 변수를 늦게 정의하라.

이 항목의 제안사항은 이해하기 쉽습니다.

 

변수를 늦게 정의한다면 만약 그 전에 함수가 return되었을 때 변수에 대한 생성자와 소멸자가 호출되지 않을 것이기 때문에 이득을

볼 수 있습니다. (exception 포함)


Item27: 캐스팅을 최소화하라.

dynamic_cast의 경우 RTTI를 사용하기 때문에 꺼려야하는 것은 일반적으로 알려진 사항입니다.

추가적으로, 이 책에서는 다른 casting들도 최소화하기를 원합니다.

 

const_cast의 경우 이미 설정한 const의 속성을 제거할 수 있기 때문에 설계상 적합하지 않을 수 있습니다.

 

static_cast는 암시적 변환이 허용되는 타입에서 이를 명시적으로 나타내는 케이스입니다.

책에서는 파생클래스 내에서 상위 클래스의 virtual 함수를 *this를 통해서 호출하는 케이스를 담았습니다.

virtual 함수 호출 시 this를 기준으로 vtable에 접근해 함수를 호출하기 때문에 적합하게 동작하지 않을 것이라는 것은 자명한 사실입니다.

 

reinterpret_cast는 억지로 캐스트 하는 것이죠,  명확한 뜻이 있는 것이 아니라면 사용하면 안됩니다.


Item28: 객체 내부 데이터의 핸들 반환을 피하라

객체의 내부 데이터의 핸들을 직접 반환한다면 캡슐화가 깨질 수 있다.

const객체이더라도 해당 객체 내의 set등의 상태변경 함수가 있다면 이를 통해 변경이 가능합니다.

 

결론적으로 객체의 내부요소에 대한 핸들(참조자, 포인터, 반복자)을 반환하는 것을 피하자는 것입니다.


Item29: 예외 안전 코드를 지향하라.

exception safety를 위해 예외 안전 코드를 지향해야 합니다.

 

여기서는 3가지의 보증 방법을 설명합니다.

 

1단계 basic guarantee: 예외가 발생해도 프로그램 자체에는 문제가 생기지 않는 보장입니다. 오브젝트 자체가 깨지지는 않지만, 예외 발생 시 관측했던 값이 변경되었을 수 있습니다.

 

2단계 strong guarantee: 예외가 발생해도 프로그램의 상태가 변경되지 않습니다. 마치 DB의 transaction과 비슷합니다.

함수 호출이 성공했다면 완전한 성공, 실패했다면 함수가 전혀 실행되지 않은 상황과 같습니다.

 

3단계 nothrow guarantee: 절대 함수가 예외를 던지지 않습니다. nothrow가 붙은 함수가 이에 해당한다고 볼 수 있습니다.

해당 함수에서 정말로 예외가 발생한다면, 중대한 문제이므로 프로그램 자체를 terminate시킵니다.


Item30: 인라이닝의 장단점을 이해하라.

inline은 기본적으로 컴파일러 최적화를 통해 함수 call을 하는 대신 함수 코드 자체를 호출하는 callee의 코드 안에 끼어넣어줍니다.

만약에 최적화 컴파일을 껐다면, 이 기능은 의미가 없습니다.

 

다만 추가적으로 inline은 여러 TU에 있어도 하나로 취급하는 처리를 해주기 때문에 헤더에 함수를 정의할 수 있습니다.

(이전 Item1에서 언급)

 

만약 inline으로 함수 호출이 최적화되었다면 이를 통해 함수 호출 오버헤드를 줄일 수 있습니다.

다만 호출하는 원 함수 본문 자체가 커지기 때문에 이를 통해 페이징 혹은 캐시히트가 저하될 수 있습니다.


Item31: 파일 간의 컴파일 종속성을 최소화하라.

하나의 파일을 수정했는데 많은 파일이 다시 재컴파일되는 경험을 해본적이 있을겁니다.

공부를 하는 입장에서 모든 파일이 다시 재컴파일된다고 해도 많은 시간이 걸리지 않지만,

거대한 프로젝트에서 이러한 상황이 발생한다면 정말 오랜시간이 걸릴 수 있습니다.

 

따라서 파일 간의 종속성을 줄이는 것은 개발 생산성에 큰 도움이 될 수 있습니다.

 

해당 책에서는 pImpl패턴을 통해 종속성 줄이기를 시도합니다.


#include <memory>  // 표준 라이브러리 - 직접 선언하지 말 것

class PersonImpl;  // Person 구현 클래스의 전방 선언
class Date;        // Date 클래스의 전방 선언
class Address;     // Address 클래스의 전방 선언

class Person {
public:
    Person(const std::string& name, const Date& birthday,
           const Address& addr);
    std::string name() const;
    std::string birthDate() const;
    std::string address() const;
    ...
private:
    std::tr1::shared_ptr<PersonImpl> pImpl;  // pImpl을 사용하여 구현 분리
};

  

위처럼 Person에 personImpl 포인터를 넣고 실제 기능을 하는 함수 구현은 personImpl에 하며, person.cpp에서는 personImpl의 함수를 호출하는 wrapping함수들만 넣습니다.

 

이러하면 personImpl의 기능을 사용하는 파일들은 #include Person을 하고 Person의 함수를 호출하게됩니다.

PersonImpl내의 함수가 수정되더라도, 외부 wrapping함수의 시그니처가 변하지 않는다면 person을 참조하는 파일들의 재컴파일은 일어나지 않습니다.

 

물론 personImpl의 시그니처는 수정되며, person의 시그니처는 수정되지 않는 형태에서 가장 의미가 있기 때문에 고려가 필요할 것입니다. 


핸들 클래스와 인터페이스 클래스는 다르며 아래와 같은 특징이 있습니다.

 

핸들 클래스 vs 인터페이스 클래스

특징 핸들 클래스 (Handle Class, pImpl) 인터페이스 클래스 (Interface Class)
데이터 저장 방식 내부적으로 pImpl 객체를 생성하여 데이터 저장 데이터 없음 (순수 가상 함수만 존재)
컴파일 의존성 내부 구현을 감출 수 있어 컴파일 속도 향상 파생 클래스가 필요하므로 유연성 증가
객체 관리 직접 객체를 생성하고 내부에서 관리 팩토리 함수 또는 스마트 포인터 사용
장점 - 구현을 숨길 수 있음
- 변경이 발생해도 클라이언트 코드 영향 적음
- 상속을 통해 확장 가능
- 순수 가상 함수만 포함하여 설계가 깔끔
단점 - 별도의 pImpl 클래스 필요 - 직접 객체 생성 불가능
- 항상 스마트 포인터 또는 팩토리 함수 필요