이펙티브 C++
https://www.yes24.com/Product/Goods/17525589
페이지 270p ~ 279p
이펙티브 C++(270p ~ 270p)
요약
항목 37: 어떤 함수에 대해서도 상속받은 기본 매개변수 값은 절대로 재정의하지 말자
- C++에서 상속받을 수 있는 함수의 종류는 가상 함수와 비가상 함수뿐이다.
- 비가상 함수는 언제라도 재정의해서는 안 되는 함수이다.
- 따라서 기본 매개변수 값을 가진 가상 함수를 상속하는 경우에 이를 절대로 재정의하지 않는 것으로 범위를 좁힐 수 있다.
- 가상 함수는 동적으로 바인딩되지만, 기본 매개변수 값은 정적으로 바인딩된다.
- 객체의 정적 타입(static type)은 프로그램 소스 안에 있는 선언문을 통해 그 객체가 갖는 타입이/다.
// 정적 타입 Shape*
Shape *ps;
// 정적 타입 Shape*
Shape *pc = new Circle;
// 정적 타입 Shape*
Shape *pr = new Rectangle;
- ps, pc, pr은 모두 Shape에 대한 포인터로 선언되어 있기 때문에 각각의 정적 타입은 모두
Shape *
타입이다. (가리키는 대상에 무관하게 정적 타입이Shape *
) - 객체의 동적 타입(dynamic type)은 현재 그 객체가 진짜로 무엇이냐에 따라 결정되는 타입이다.
- 다시 말해 ‘이 객체가 어떻게 동작할 것이냐’를 가리키는 타입이 동적 타입이다.
- pc의 동적 타입은
Circle *
이고 pr의 동적 타입은Rectangle *
이다. - ps는 동적 타입이 없다.(아무 객체도 참조하지 않고 있음)
- 동적 타입은 프로그램이 실행되는 도중에 바뀔 수 있다.
ps = pc;
ps = pr;
- 가상 함수는 동적으로 바인딩된다. 가상 함수의 호출이 일어난 객체의 동적 타입에 따라 어떤 가상 함수가 호출될지 결정된다는 이야기이다.
pc->draw(Shape::Red); // Circle::draw(Shape::Red) 호출
pr->draw(Shape::Red); // Rectangle::draw(Shape::Red) 호출
- 문제는 기본 매개변수 값이 설정된 가상 함수에서 발생한다.
- 가상 함수는 동적으로 바인딩되어 있지만 기본 매개변수는 정적으로 바인딩되어 있기 때문이다.
- 파생 클래스에 정의된 가상 함수를 호출하면서 기본 클래스에 정의된 기본 매개변수 값을 사용할 수 있다는 이야기이다.
class Shape {
public:
enum ShapeColor { Red, Green, Blue };
virtual void draw(ShapeColor color = Red) const;
}
class Rectangle: public Shape {
public:
// 기본 매개변수 값이 Green으로 설정됨
virtual void draw(ShapeColor color = Green) const;
}
pr->draw(); // Rectangle::draw(Shape::Red)가 호출됨!
- pr의 동적 타입이
Rectangle *
이므로 호출되는 가상 함수는 Rectangle의 것이다. - Rectangle::draw 함수에서는 기본 매개변수 값이 Green으로 설정되어 있다.
- 하지만 pr의 정적 타입은
Shape*
이므로 지금 호출되는 가상 함수에 쓰이는 기본 매개변수 값을 Shape 클래스에서 가져온다. - 이는 포인터가 아니라 참조자에서도 동일하게 발생하는 문제이며, 가상 함수에 사용되는 기본 매개변수 값을 파생 클래스에서 재정의 했다는 것이 문제의 원인이다.
- 함수의 기본 매개변수가 동적으로 바인딩된다면, 프로그램 실행 중에 가상 함수의 기본 매개변수 값을 결정할 방법을 컴파일러 쪽에서 마련해주어야 한다.
- 현재의 컴파일러는 속도 유지와 구현 간편성에 무게를 더 두었기 때문에 지금과 같이 기본 매개변수를 정적으로 바인딩한다.
- 기본 매개변수 값을 똑같이 제공하면 다음과 같이 코드를 작성할 수 있는데, 아직도 문제가 존재한다.
class Shape {
public:
enum ShapeColor { Red, Green, Blue };
virtual void draw(ShapeColor color = Red) const;
}
class Rectangle: public Shape {
public:
virtual void draw(ShapeColor color = Red) const;
}
- 문제는 다음과 같다
- 코드가 중복되고 있다.
- 코드 중복에 의존성이 있어 Shape 클래스에서 기본 매개변수 값이 변경되면 파생 클래스는 모두 그 값을 변경해야 한다.
- 가상 함수가 기본 매개변수 값에 대하여 기본 클래스의 값을 참조하지 않게 하려면 NVI 관용구를 사용해볼 수 있다.
class Shape {
public:
enum ShapeColor { Red, Green, Blue };
void draw(ShapeColor color = Red) const {
doDraw(color);
}
private:
virtual void doDraw(ShapeColor color) const = 0;
}
class Rectangle: public Shape {
private:
virtual void doDraw(ShapeColor color) const;
}
- NVI 관용구를 적용하면 비가상 함수가 기본 매개변수를 지정하도록 만들 수 있다.
- 비가상 함수는 파생 클래스에서 오버라이드되면 안 되기 때문에 위와 같이 설계하면 draw 함수의 color 매개변수에 대한 기본값을 Red로 고정시킬 수 있다.
항목 38: has-a 혹은 is-implement-in-terms-of 를 모형화할 때는 개체 합성을 사용하자
- 합성이란, 어떤 타입의 객체들이 그와 다른 타입의 객체들을 포함하고 있을 경우에 성립하는 그 타입들 사이의 관계를 일컫는다.
class Address {};
class PhoneNumber {};
class Person {
public:
private:
// Person 클래스를 이루는 객체 중 하나
std::string name;
Address address;
PhoneNumber voidNumber;
PhoneNumber faxnumber;
};
- Person 객체는 string, Address, PhoneNumber 객체로 이루어져 있다.
- 합성이라는 단어 대신 레이어링, 포함, 통합, 내장 등으로도 표현할 수 있다.
- 객체의 합성은 다음과 같은 의미를 가진다.
- has-a
- is-implement-in-terms-of
- 객체 중에는 우리 일상생활에서 볼 수 있는 사물을 본 뜬 것들이 있다.
- 이런 것들에는 사람, 이동수단, 비디오 프레임 등이며, 이런 객체는 응용 영역에 속한다.
- 응용 영역에 속하지 않는 나머지들은 버퍼, 뮤텍스, 탐색 트리 등 순수하게 시스템 구현만을 위한 인공물이다.
- 이런 것들을 구현 영역에 속한다고 부른다.
- 객체의 합성이 응용 영역의 객체들 사이에서 일어나면
has-a
관계를 가진다. - 객체의 합성이 구현 영역에서 일어나면
is-implement-in-terms-of
관계를 가진다. - 따라서 Person 클래스가 나타내는 관게는 has-a 관계라고 볼 수 있다.
- is-implement-in-terms-of 관계는 다음의 예제를 보자.
Set 템플릿을 list 템플릿으로 만들기
- Set 템플릿을 만들어야 하는 상황이다.
- 코드의 재사용성을 위해 표준 C++ 라이브러리에 있는 list 템플릿을 재사용하려고 한다.
- 그래서 다음과 같이 list의 파생 형태로 Set 템플릿을 만들었다.
template<typename T>
class Set : public std::list<T> {};
- 그러나 이 코드는 다음과 같은 문제를 가지고 있다.
- public 상속은 is-a 관계를 가진다. 따라서 B에서 참인 것들이 전부 D에서도 참이어야 하는데, list 객체는 중복 원소를 가질 수 있지만 Set 객체는 중복 원소를 허용하지 않는다.
- 따라서 Set이 list의 일종(is-a)이라는 명제는 거짓이 된다.
- 결과적으로 두 클래스 사이의 관계가 is-a가 될 수 없으므로 public 상속은 지금의 관계를 모형화하는 데 맞지 않다.
- 그래서 Set 객체는 list 객체를 써서 구현되는(is implemented in terms of) 형태로 설계가 가능하다.
template<class T>
class Set {
public:
bool member(const T& item) const;
void insert(const T& item);
void remove(const T& item);
std::size_t size() const;
private:
std::list<T> rep;
};
- 이렇게 선언된 Set의 구현부는 표준 라이브러리의 일부를 잘 가져다 사용하면 될 정도로 간단해진다.
메모
항목 37: 어떤 함수에 대해서도 상속받은 기본 매개변수 값은 절대로 재정의하지 말자
- 상속받은 기본 매개변수 값은 절대로 재정의해서는 안 된다. 왜냐하면 기본 매개변수 값은 정적으로 바인딩되는 반면, 가상 함수는 동적으로 바인딩되기 때문이다.
항목 38: has-a 혹은 is-implement-in-terms-of 를 모형화할 때는 개체 합성을 사용하자
- 객체 합성의 의미는 public 상속이 가진 의미와 완전히 다르다.
- 응용 영역에서 객체 합성의 의미는 has-a이다. 구현 영역에서는 is-implemented-in-terms-of(..는 ..를 써서 구현됨)의 의미를 갖는다.
'책 > 이펙티브 C++' 카테고리의 다른 글
이펙티브 C++(295p ~ 306p) (0) | 2024.02.05 |
---|---|
이펙티브 C++(279p ~ 290p) (1) | 2024.02.03 |
이펙티브 C++(261p ~ 270p) (0) | 2024.02.01 |
이펙티브 C++(250p ~ 261p) (0) | 2024.01.31 |
이펙티브 C++(230p ~ 239p) (0) | 2024.01.30 |