ABOUT ME

-

Today
-
Yesterday
-
Total
-
  • [Effective C++] 5장(2) - 구현
    쾌락없는 책임 (공부)/Effetive C++ 요약본 2022. 5. 16. 17:49
    반응형
    본 카테고리는 프로텍 미디어의 '이펙티브 C++'을 보고 요약하는 카테고리입니다.

    3판을 기준으로 하며 전체 내용이 아닌 간략한 내용만을 요약하고 있습니다.

     

     

    항목 29 : 예외 안정성이 확보되는 그날 위해 싸우고 또 싸우자

     이런 함수가 있다고 생각해 봅시다.

    void PrettyMenu::ChangeBackGround(std::istream& imgSrc)
    {
    	lock(&mutex);
    
    	delete bgImage;
    	++imageChanges;
    	bgImage = new Image(imgSrc);
    
    	unlock(&mutex);
    }

     위 함수의 경우 예외 안정성에 있어서 최악의 함수가 되는 함수입니다. 일단 예외 안정성을 가지는 함수는 자원이 새지 않고 자료구조가 더럽혀지지 않아야 합니다. 위의 경우 new 에서 예외가 생기면 락을 풀지 못하는 문제가 있으며 같은 예외에서 할당이 되지 않았는데 변수를 ++ 하는 문제가 있습니다.

     

     위에서 자원 누수 문제는 항목 13, 14를 잘 따라하면 됩니다.

    void PrettyMenu::ChangeBackGround(std::istream& imgSrc)
    {
    	lock ml(&mutex);
    
    	delete bgImage;
    	++imageChanges;
    	bgImage = new Image(imgSrc);
    }

     lock 클래스를 통해서 함수 코드 길이도 줄이고 자동으로 unlock을 할 수 있죠.

     

     자료구조 오염의 경우 일단 알아보기 전에 용어 공부를 좀 해야 합니다. 예외 안정성을 갖춘 함수는 아래 3가지 보장중 하나를 제공합니다.

     

    <기본적인 보장>

    - 함수 동작중 예외 발생시 프로그램에 관한 모든걸 유효한 상태로

    - 객체나 자료구조도 더럽혀지지 않으며 모든 객체의 상태는 내부 일관성을 유지합니다.

    - 단 프로그램 상태는 예측이 안됩니다.

     

    <강력한 보장>

    - 예외가 발생하면 프로그램 상태를 변경하지 않겠다는 보장

    - 이런 함수를 호출하는건 원자적인 동작이라고 합니다.

    - 호출 실패시 호출 이전으로 프로그램이 돌아갑니다.

    - 사용자 입장에서는 상태가 2개(실행을 마친후, 호출될때 상태)이므로 예측이 좀 더 쉽습니다.

     

    <예외불가 보장>

    - 예외를 던지지 않고 언제나 이를 오나성 하겠다는 것입니다.

    - 주로 기본제공 타입에 대한 연산들이 해당됩니다.

     

     

     위 여러 보장중 어떤걸 할지 선택을 해야 합니다. 대부분의 경우 기본, 강력한 보장을 사용하게 됩니다. 위 함수를 강력한 보장을 한다고 가정하고 고쳐보겠습니다.

    class PrettyMenu {
    	...
    	std::tr1::shared_ptr<Image> bgImage;
    };
    
    void Pretty::ChangeBackGround(std::istream& imgSrc)
    {
    	Lock ml(&mutex);
    	bgImage.reset(new Image(imgSrc));
    
    	++imageChanges;
    }

     shared_ptr을 통해서 자원관리를 할 수 있게 변경을 했으며 함수 내 문장을 재배치해 동작이 전부 완료가 되면 카운트 변수를 증가시켰습니다. Image 객체를 개발자가 삭제할 필요도 없으며 reset이 제대로 불릴려면 new 가 제대로 되어야 합니다.

     

     대신 살짝의 옥의 티가 imgSrc에 있는데 Image 클래스 생성자가 예외를 일으키면 그 시점 입력 스트림의 읽기 표시자가 이동했을 수 있고 이게 어떤 영향을 미칠지 알 수 없습니다. 이 문제가 있다는 것만 알고 다른 상황을 보겠습니다.

     

     이번에는 예외에 속수무책인 함수를 강력한 보장을 하는 함수로 바꾸는 전략을 소개하는데 전략의 이름은 복사 후 맞바꾸기 라고 합니다. 객체를 수정하고 싶다면 사본을 만든 뒤 그 사본을 수정하는 것 입니다. 이전에 있던 pimpl을 보고 오면 좋을 것 같습니다.

     

    C++ pimpl과 관련한 이야기들

    개요  C++에 대한 이해도를 더 높여보기 위해서 Effective C++을 읽던 도중 pimpl 구조라는 흥미로운 키워드를 보게 되었습니다. 구현부는 따로 저장하고 이 객체의 주소를 가리키는 포인터를 사용하

    husk321.tistory.com

     이렇게 복사 후 바꾸는 전략은 전부 바뀌거나 안바뀌는 경우 아주 좋습니다. 그런데 함수 전체가 강력한 예외 안정성을 보장하지 않는다는 이야기가 있습니다.

    void SomeFunc(){
    	...  // 사본 만들기
    	f1();
    	f2();
    	...  // 변경된 상태 바꾸기
    }

     함수 안에 있는 함수들이 예외 안정성이 좋지 않으면 SomeFunc도 예외 안정성이 좋지 않은 것이죠.

     

     여기서 불거지는 문제가 함수의 부수효과(side effect)로 자신의 것만 바꾸는건 쉽지만 비지역 데이터에 대한 부수효과를 주는 함수는 보장이 까다롭습니다. 또 복사 후 맞바꾸기 전략이 효율적인 전략은 아니라서 예외 안정성이 언제나 실용적인 것은 아닙니다.

     

     이제 기본적인 보장에 눈을 돌려 일단 실용성을 챙긴 뒤 이후 강력한 보장을 제공하면 됩니다. 항상 고민하는 버릇을 들이고 예외 안정성을 보장하지 못했다면 문서로 남겨 이후 개발자가 잘 사용할 수 있게 하면 됩니다.

     

    항목 30 : 인라인 함수는 미주알고주알 따져서 이해해 두자

     인라인 함수는 오버헤드도 줄고 매크로보다 안정성이 있습니다. 대신 호출문을 함수 본문으로 바꾸는 거라 목적 코드가 커질 우려가 있어 메모리가 제한된 컴퓨터에서는 벗어날 수도 있습니다. 반대로 본문 길이가 짧은 함수의 경우 이득이 되어 목적 코드도 작아지고 명령어 캐시 힛 횟수도 늘어나게 됩니다.

     

     그리고 inline 키워드는 확정이 아닌 요청으로 될수도 있고 안될수도 있습니다. 컴파일러가 inline 키워드를 보고 넣어줄수도 있고 안넣어줄수도 있죠.

    class Person{
    public:
    	...
    	int age() const { return theAge; }  // 암시적인 인라인 요청
    	...
    private:
    	int theAge;
    };
    
    
    // stl max
    tmplate<typename T>
    inline const T& std::max(...) {...}

     위 코드가 예시인데 위 예시를 보고 함수 템플릿은 반드시 인라인이어야 한다 라는건 잘못된 생각입니다. 인라인 함수는 해더 파일에 들어 있는게 맞는데 이는 대부분의 빌드 환경에서 인라인을 컴파일 도중 수행하기 때문입니다. 템플릿 역시 대체적으로 해더에 들어있는데 컴파일러가 이를 알아내야 하기 때문입니다.

     

     위 설명을 보면 인라인과 함수 템플릿은 아무 관계가 없습니다. 그냥 템플릿을 만드는데 이걸로 만들어지는 함수가 모두 인라인이면 좋을 것 같다면 inline을 붙이는 것이죠. 아닌 경우에는 inline에 비용이 드니 선언하지 않는게 좋습니다.

     

    <대부분의 빌드 환경에서 인라인이 안되는 것>

    - 루프나 재귀가 있는 함수

    - 가상 함수

    - 함수 포인터로 인라인 함수를 호출할 때 포인터로 호출한 함수

     

     마지막으로 생성자와 소멸자는 인라인하기 좋은 함수가 아닙니다.

    class Base{
    public:
    	...
    private:
    	std::string bm1, bm2;
    };
    
    class Derived : public Base{
    public:
    	Derived();   // 정말 생성자가 비어있을까요?
    ...
    private:
    	std::string dm1, dm2, dm3;

     Derived 생성자를 인라인 하고 싶은데 그러면 안됩니다! 객체 생성, 소멸마다 인라인화가 되면 본문이 엄청 길어질 예정이니깐요!

     

     그러니 꼭 인라인 해야 하는 함수나 단순한 함수, 업데이트가 잘 안되는 함수만 인라인화 하는걸로 합시다!

     

     

     

    항목 31 : 파일 사이의 컴파일 의존성을 최대로 줄이자

     클래스를 살짝 바꾼뒤 빌드하면 느낌이 이상한 경우가 있습니다. 여기의 핵심은 C++이 인터페이스와 구현을 깔끔하게 분리하는데 일가견이 없다는 것입니다.

     

     혹시 include문을 위에 넣으면 헤더 파일들 간 컴파일 의존성을 엮게 됩니다.

    namespace std {
    	class string;   // 전방선언 : 일단 틀렸습니다.
    }
    
    class Date;
    class 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;
    }

     다른 클래스들을 위해 전방 선언을 했고 구현 세부 사항을 따로 떨어뜨렸습니다. 그런데 틀린 코드 입니다. string 은 클래스가 아니라 typedef로 정의한 타입동의어로 전방선언이 안됩니다. 

     그리고 두번째 문제는 일단 컴파일러가 컴파일시 크기를 알아야한다는 것입니다. 만일 Person을 사용한다면 Person을 하나 담을 객체를 만들어야 하는데 전방선언으로 세부사항이 빠져 공간을 잘 모르게 됩니다.

     이는 Java, C# 처럼 포인터 뒤에 숨기기를 할 수 있습니다.

    int main(){
    	Person *p;
    }

     이걸 본 김에 방법을 더 알아보겠습니다.

     

     일단 클래스를 두개로 쪼개 하나는 인터페이스만 하나는 구현을 맡습니다.

    #include <string>
    #include <memory>
    
    class PersonImpl;    // Person의 구현 클래스에 대한 전방 선언
    // 클래스 안에 사용하는 것들에 대한 전방 선언
    class Date;
    class 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 구조로 사용하면 되는 것이죠. 그리고 여기서 포인터 이름을 주로 pImpl로 하는 것도 관용처럼 사용됩니다. 이런 pImpl 구조는 세부사항을 고쳐도 사용자는 재 컴파일할 필요가 없습니다. 그래서 이런 정의부에 대한 의존성을 선언부에 대한 의존성으로 바꾸어 컴파일 의존성을 최소화 했습니다. 이게 핵심 원리죠.

    반응형

    댓글

Designed by Tistory.