[C++] 스터디 CPPALOM 14주차: Effective C++ Item 26~35
파워 포인트 파일(.pptx)을 Markdown으로 변환하여 업로드하였음)
# <br>CPPALOM # <br>14주차 – Effective C++ item 26~35 한수빈 # <br>항목26: 변수 정의는 늦출 수 있는 데까지 늦추는 근성을 발휘하자 * 변수는 사용되기 직전에 정의되어야 한다. * 예외 등으로 인해 해당 변수가 사용되지 않음에도 소멸자 등이 호출되는 비용이 발생되기 때문이다! * 루프에서 변수를 다루는 방법 * 루프 바깥쪽에 정의하기: 생성자 1번 + 소멸자 1번 + 대입 n번 * 루프 안쪽에 정의하기: 생성자 n번 + 소멸자 n번 ```C++ Widget w; for (int i = 0; i < n; ++i) { w = i; ... } ``` ```C++ for (int i = 0; i < n; ++i) { Widget w(i); } ``` # <br>항목27: 캐스팅은 절약, 또 절약! 잊지 말자 * 구형 스타일의 캐스트를 사용하지 말자! * dynamic_cast는 성능 저하가 있으므로, 이를 최소화하자! * 최악: 폭포식 dynamic_cast * 수빈의 첨언: * 상속 구조에서 특정한 타입인지를 반드시 알아야되는 구조라면, 리스코프 치환 원칙(LSP) 위배일 가능성이 높다. dynamic_cast는 필요한 경우가 있지만 이에 주의하자! ```C++ for(auto iter = winPtrs.begin(); iter != winPtrs.end(); ++iter) { if(SpecialWindow1 *psw1 = dynamic_cast<SpecialWindow*>(iter->get())) {...} else if (SpecialWindow2 *psw2 = dynamic_cast<SpecialWindow2*>(iter->get())) {...} else if (SpecailWindow3 *psw3 = dynamic_cast<SpecialWindow3*>(iter->get())) {...} ... } ``` # <br>항목28: 내부에서 사용하는 객체에 대한 ‘핸들'을 반환하는 코드는 되도록 피하자 * 추상화된 객체에서 세부 구현에 대한 포인터를 반환하지 말자. * upperLeft(), lowerRight()는 const 함수임에도 불구하고 외부의 클라이언트가 해당 RectData를 조작할 수 있게 만든다. * 분명 Rectangle 클래스에 대한 비트적 상수성은 지켜지고 있으나, 논리적 상수성을 위배하게 된다! * 따라서 상수 포인터를 넘기는 것 이외에는 이러한 행위를 자제하자. ```C++ class Rectangle { public: Point& upperLeft() const { return pData->ulhc; } Point& lowerRight() const { return pData->lrhc; } private: std::shared_ptr<RectData> pData; } ``` ```C++ class Rectangle { public: const Point& upperLeft() const { return pData->ulhc; } const Point& lowerRight() const { return pData->lrhc; } private: std::shared_ptr<RectData> pData; } ``` * 또한 dangling handle을 반환할 여지가 있다. * boundingBox()를 통해 반환된 Rectangle 객체는 임시 객체이다. * 임시 객체의 핸들이 upperLeft()를 통해 반환되고, * 그것이 클라이언트의 포인터 변수인 pUpperLeft에 대입된다. * 임시 객체는 수명을 다하였으므로 소멸되며, * pUpperLeft는 dangling pointer가 된다. ```C++ const Rectangle boundingBox(const GUIObject& obj); … GUIObject *pgo; const Point *pUpperLeft = &(boundingBox(*pgo).upperLeft()); ``` # <br>항목29: 예외 안전성이 확보되는 그날 위해 싸우고 또 싸우자! * 다음 코드는 최악의 예외 안전성을 보여준다! * 예외 안전성을 가진 함수라면 예외가 발생할 때 다음과 같이 동작해야 한다: * 자원이 새도록 만들지 않는다. * 자료구조가 더럽혀지는 것을 허용하지 않는다. ```C++ void PrettyMenu::changeBackground(std::istream& imgSrc) { lock(&mnutex); delete bgImage; ++imageChanges; bgImage = new Image(imgeSrc); unlock(&mutex); } ``` * 자원 관리 객체를 이용해서 mutex의 unlock 문제를 해결할 수 있다. * 예외 안전성을 갖춘 함수는 다음 세 가지 중 하나를 보장한다. * 기본적인 보장: 함수 동작 중에 예외가 발생하면, 실행 중인 프로그램에 관련한 모든 것들을 유효한 상태로 유지한다. * 강력한 보장: 함수 동작 중에 예외가 발생하면, 프로그램의 상태를 절대로 변경하지 않는다. * 예외불가 보장: 예외를 절대로 던지지 않는다. * 강력한 보장을 지키는 것이 적절한 타협안이 될 것이다. ```C++ void PrettyMenu::changeBackground(std::istream& imgSrc) { Lock ml(&mutex); delete bgImage; ++imageChanges; bgImage = new Image(imgeSrc); } ``` 기본적인 보장을 지키도록 바꾼 코드는 다음과 같다: 스마트 포인터가 객체의 생명주기를 안전하게 관리하므로, reset() 호출 이전과 reset() 호출 이후만이 원자적인 상태로서 남는다. ```C++ std::shared_ptr<Image> bgImage; … void PrettyMenu::changeBackground(std::istream& imgSrc) { Lock ml(&mutex); bgImage.reset(new Image(imgeSrc)); ++imageChanges; } ``` * 강력한 보장을 지키도록 바꾼 코드는 다음과 같다: * copy-and-swap 기법은 언제나 사본을 만들고 그것을 조작한 뒤에 마지막에 원본과 바꿔치기 하는 것이다. * 이는 강력한 보장을 지키기에 적절한 기법이다. ```C++ std::shared_ptr<PMImpl> pImpl; void PrettyMenu::changeBackground(std::istream& imgSrc) { using std::swap; Lock ml(&mutex); // copy std::shared_ptr<PMImpl> pNew(new PMImpl(*pImpl)); // manipulate pNew->bgImage.reset(new Image(imgeSrc)); ++pNew->imageChanges; // swap swap(pImpl, pNew); } ``` * 하지만 모든 함수의 예외 안전성이 강력하다는 보장이 없기 때문에, 다음과 같은 코드에서는 문제가 발생한다: * f1()과 f2()가 강력한 예외 안전성을 보장하지 않으므로, side effect가 발생한다. * 이는 현실적인 문제이며, 실제로 이러한 문제 때문에 강력한 예외 안전성을 보장하기 어렵다. * 하지만 우리는 최대한 강력한 예외 안전성을 보장하려고 노력해야 한다! ```C++ void someFunc() { ... f1(); f2(); ... } ``` # <br>항목30: 인라인 함수는 미주알고주알 따져서 이해해 두자 * 먼저, 인라인의 여부는 컴파일러에게 달렸음을 명심하자. * 인라인의 trade-off: * 인라인은 함수 코드를 그대로 각각의 함수 호출 코드에 삽입해버리는 것이므로, * 함수 호출의 오버헤드를 줄일 수 있다. * 단, 목적 코드가 비정상적으로 불어날 수 있다. * 곧, 캐싱에도 효율이 떨어질 수 있으며 성능 저하로 이어질 수 있다. * 클래스 선언 시 직접 함수를 정의하면 암시적인 인라인이 된다! * 혹은 inline 키워드를 사용하면 된다. * 함수 인라인은 작고, 자주 호출되는 함수에 대해서만 하는 것으로 생각하자. * 인라인을 남용하지 말 것! # <br>항목31: 파일 사이의 컴파일 의존성을 최대한 줄이자 * 객체를 생성하기 위해서는 할당을 위해 메모리 구조를 알아야 한다. * 어떤 객체들을 멤버로 가지고 있는지: 다음 코드는 컴파일 될 수 없다. * stirng, Date, Address가 어떻게 정의되어 있는지 모르기 때문이다! * 이러한 컴파일 의존성은 빌드 타임을 증가시킨다. * #include 지시자의 헤더들이 하나라도 바뀌게 되면, 해당 파일은 다시 컴파일되어야 한다! ```C++ class Person { public: Person(const std::string& name, const Date& birthday, const Address& adr); std::string name() const; std::string birthDate() const; std::string address() const; private: std::String theName; Date theBirthDate; Address theAddress; } ``` * 좋은 해결책: 포인터를 사용하자! * 포인터는 전부 같은 크기를 가지며, 따라서 메모리에 할당할 때 별도의 객체 구조를 알 필요가 없다. * pimpl 관용구: Java의 interface를 구현하는 하나의 해결책 * 구현이 바뀌었을 때 PersonImpl 클래스가 컴파일이 다시 되는 것은 당연하다! * 단, Person을 include하는 수많은 클래스들은 다시 컴파일될 필요가 없다. ```C++ #include <memory> #include <string> class PersonImpl; class Date; class Address; class Person { public: Person(const std::string& name, const Date& birthday, const Address& adr); std::string name() const; std::string birthDate() const; std::string address() const; private: std::shared_ptr<PersonImpl> pImpl; }; ``` * 객체 참조자 및 포인터로 충분한 경우에는 객체를 직접 쓰지 않는다. * 포인터 정의 시 해당 타입의 선언부만 include 한다. * 할 수 있으면 클래스 정의 대신 클래스 선언에 최대한 의존하도록 만든다. * 특히, 함수를 선언할 때는 클래스의 정의를 가져오지 않아도 된다. * 오해 금물: today()를 사용하려면 결국 Date 클래스의 정의를 include 해야 한다. * 이는 today(), clearAppointments()의 사용자 측에서 필요에 따라 하게 된다! * 어떤 라이브러리를 만들었을 때, 수많은 함수들을 제공하게 된다. * 이들을 전부 사용하지 않음에도 라이브러리에 관련된 모든 클래스를 컴파일해야 하는 필요를 없앤다. ```C++ class Date; // 클래스 전방 선언 Date today(); // OK, Date 클래스의 void clearAppointments(Date d); // 정의를 가져오지 않아도 된다. ``` * 선언부와 정의부에 대해 별도의 헤더 파일을 제공한다. * 사용자는 선언부만을 includ해서 사용한다. * 예시: * Main.cpp * Person.h * PersonImpl.h * Person.cpp ```C++ #include <string> #include "Date.h" class PersonImpl { public: PersonImpl(const std::string& name, const Date& birthday); std::string name() const; std::string birthDate() const; private: std::string theName; Date theDate; }; ``` ```C++ #include "Person.h" #include "Date.h" int main() { Person p("Subin", Date(30)); … } ``` ```C++ #include "Person.h" #include "PersonImpl.h" Person::Person(const std::string& name, const Date& birthday) : pImpl(new PersonImpl(name, birthday)) { } std::string Person::name() const { return pImpl->name(); } … ``` ```C++ class PersonImpl; class Date; class Person { public: Person(const std::string& name, const Date& birthday); std::string name() const; std::string birthDate() const; private: std::shared_ptr<PersonImpl> pImpl; }; ``` * 인터페이스 클래스와 팩토리 함수를 활용할 수도 있다. * 사용자는 추상 클래스인 Person에만 의존하며 세부 구현은 동적으로 할당되어 활용된다! * 선언과 정의의 분리에 대한 비용: * 핸들 클래스의 경우: 포인터에 대한 메모리와 성능, 생성자 및 소멸자 호출 오버헤드 * 인터페이스의 경우: 가상 테이블 오버헤드 * 공통: 인라인 함수의 도움을 얻기 어려움! ```C++ class Person { public: virtual ~Person(); virtual std::string name() const = 0; virtual std::string birthDate() const = 0; virtual std::string address() const = 0; static std::shared_ptr<Person> create( const std::string& name, const Date& birthday, const Address& addr); }; ``` # <br>항목32: public 상속 모형은 반드시 is-a를 따르도록 만들자 * 자식 클래스는 부모 클래스여야 한다. * 부모 클래스는 자식 클래스이지 않아도 된다. * 가령, 학생(자식)은 사람(부모)이지만, 사람(부모)는 학생(자식)이 아닐 수도 있다. * 주의해야 할 사항: 프로그램 관점에서의 is-a는 현실과 다를 수도 있다! * 리스코프 치환 원칙(LSP)를 지켜야 한다. * 다음은 리스코프 치환 원칙이 지켜지지 않는다: 정사각형은 사각형이지만, 프로그램 관점에서 정사각형의 변 길이를 다루는 것이 부모의 구현과 충돌한다. ```C++ class Rectangle { public: virtual void setHeight(int newHeight); virtual void setWidth(int newWidth); virtual int height() const; virtual int width() const; } void makeBigger(Rectangle& r) { int oldHeight = r.height(); r.setWidth(r.width() + 10); assert(r.height() == oldHeight); } ``` ```C++ class Square : public Rectangle{}; Square s; assert(s.width() == s.height()); makeBigger(s); assert(s.width() == s.height()); ``` # <br>항목33: 상속된 이름을 숨기는 일은 피하자 * 오버로딩된 함수를 자식 클래스에서 오버라이딩할 때에는 주의하라! * 파생 클래스의 이름은 기본 클래스의 이름을 가린다. public 상속에서 이러한 이름 가림 현상은 바람직하지 않다. * 가려진 이름을 다시 볼 수 있게 하는 방법으로 using 선언 혹은 전달 함수를 쓸 수 있다. * 이름 가림 * 다음과 같은 상황에서, 클라이언트는 Derived 객체의 mf1(int)를 호출할 수 없다. ```C++ class Base { public: virtual void mf1() = 0; virtual void mf1(int); ... }; class Derived : public Base { public: virtual void mf1(); } ``` ```C++ Derived d; int x; d.mf1(); // OK. Call the Derived::mf1(). d.mf1(x); // Error! Derived::mf1(int) is not defined. ``` * 오버로딩된 함수를 자식 클래스에서 오버라이딩할 때에는 주의하라! * 파생 클래스의 이름은 기본 클래스의 이름을 가린다. public 상속에서 이러한 이름 가림 현상은 바람직하지 않다. * 가려진 이름을 다시 볼 수 있게 하는 방법으로 using 선언 혹은 전달 함수를 쓸 수 있다. * 전달 함수 * 상황: private 상속을 하였으며 오버로딩된 mf1 중 단 하나만 사용하게 만들고 싶다. * 만약 using Base::mf1;을 활용할 경우 오버로딩된 mf1(int)도 드러나게 된다. * 대신 전달 함수를 활용해 하나만 활성화시킬 수 있다. ```C++ class Base { public: virtual void mf1() = 0; virtual void mf1(int); ... }; class Derived : private Base { public: virtual void mf1() { Base::mf1(); }} ``` # <br>항목34: 인터페이스 상속과 구현 상속의 차이를 제대로 파악하고 구별하자 * 순수 가상 함수: * 파생 클래스에게 함수의 인터페이스만을 물려준다. * 즉, 동적으로 정의되어야 하는 동작! * 단순 가상 함수: * 파생 클래스에게 인터페이스뿐만 아니라 그 함수의 기본 구현도 물려준다. * 즉, 필요에 의해 정의되어야 하는 동작! * 비가상 함수: * 함수 인터페이스와 더불어 그 함수의 필수적인 구현을 물려준다. * 즉, 변하지 않는 동작! * 편법: * 순수 가상 함수이면서도 기본 동작을 제공하는 법 * 순수 가상 함수로 선언하고, 정의를 제공해버린다! ```C++ class Airplane { public: virtual void fly(const Airport& destination) = 0; }; void Airplane::fly(const Airport& destination) { // defaultFly(); } class ModelA : public Airplane { public: virtual void fly(const Airport& destination) { Airplane::fly(destination); } }; ``` # <br>항목35: 가상 함수 대신 쓸 것들도 생각해 두는 자세를 시시때때로 길러 두자 * 템플릿 메서드 패턴 활용하기 * GameCharacter의 개발자는 healthValue를 반환하는 일종의 템플릿을 만들고, 핵심 로직은 파생 클래스들이 재정의할 수 있도록 한다. * 이를 NVI(non-virtual interface) 관용구라고도 한다! ```C++ class GameCharacter { public: int healthValue() const { ... int retVal = doHealthValue(); ... return retVal; } private: virtual int doHealthValue() const { … } } ``` * 전략 패턴 활용하기 * 함수 포인터를 이용해서 healthValue를 반환하는 함수를 동적으로 생성자를 통해 받는다. * 일반적인 객체지향 디자인 패턴과 달리, C++에서는 객체가 아닌 함수 포인터로 받아도 된다! * 더 나아가, <functional> 템플릿을 이용해 좀 더 유연한 함수를 전달받을 수 있다. * 가령, 일반적으로 클래스의 멤버로 정의된 함수를 GameCharacter의 생성자에 넘겨줄 수는 없다. 함수 호출 규약이 다르기 때문이다. * std::bind를 이용해 this 인자를 무시하고 단항 함수로 만들어 멤버 함수임에도 넘길 수 있게 된다! ```C++ class GameCharacter { public: typedef int (*HealthCalcFunc)(const GameCharacter&); explicit GameCharacter(HealthCalcFunc hcf = defaultHealthCalc) : healthFunc(hcf) {} int healthValue() const{ return healthFunc(*this); } private: HealthCalcFunc healthFunc; } ```
댓글
댓글 쓰기