[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;
}
```
댓글
댓글 쓰기