Effective C++ : Chapter 5 구현


 

 

항목 26: 변수 정의를 늦출 수 있는 데까지 늦추는 근성을 발휘하자.

 

Q.  아래 코드에서 encrypted 변수를 어떻게 초기화하고 있을까요? 아래의 보기 중 옳은 것을 고르시오.

std::string encryptPassword(const std::string& password)
{
	std::string encrypted(password); 
}

1.  string의 기본 생성자에 의해 만들어지고 password 값이 대입된다.

2.  string의 복사 생성자에 의해 초기화된다.

더보기

답:  2번 ( 185~186쪽 )

불필요한 기본 생성자 호출이 일어나지 않고 변수의 의미가 명확한 상황에서 변수를 정의함과 동시에 복사 생성자에 의해 초기화가 이루어지고 있습니다.

 

 

Q. 다음 2개의 코드 중 어떤 생성자를 몇 번씩 호출하는지 추측하시오. (단, password 생성은 고려하지 않는다).

std::string encrypted;
std::string password("abc");
encrypted = password;
std::string password("abc");
std::string encrypted(password);
더보기

답:  기본생성자 1 대입연산자 1 , 복사생성자

 

 

Q. 아래의 2가지 방법의 비용이 각각 얼마나 드는지 '생성자, 연산자, 소멸자'와 연관지어서 설명하세요.

A.

widget w;
for (int i = 0; i < n; ++i)
{
	// w = i에따라 달라지는 값;
}

B.

for (int i = 0; i < n; ++i)
{
	// Widget W(i에 따라 달라지는 값);
}
더보기

A : 생성자 1번 + 소멸자 1번 + 대입 n번
B : 생성자 n번 + 소멸자 n번

위의 답을 보고 대부분의 상황에서 A방법이 더 좋다고 생각할 수 있습니다. 그러나 A방법을 쓰면 w라는 이름을 볼 수 있는 볌위가 B 방법보다 넓어지기 때문에 프로그램의 이해도와 유지보수성이 떨어질 수 있습니다. 그러니 아래의 두 상황에서는 B방법 채택하는게 좋습니다.

 

만약 대입에 들어가는 비용이 생성자-소멸자 쌍보다 적게 나오는 경우가 있다면 A가 더 효율적이겠지만, 그렇지 않은 경 우는 B 방식이 더 효율적입니다.
유효범위 측면에서도 한번 생각해 볼 필요가 있습니다. A의 경우는 루프를 포함하는 유효범위를 가지고 있어 프로그램의 이해도와 유지보수성이 좋지 않아질 수 있습니다.


 

 

항목 27: 캐스팅은 절약, 또 절약! 잊지 말자

 

Q. cast를 사용하면 안되는 이유가 무엇일까요?

더보기

책을 읽었을 때에는 비용 문제 인 것 같기도 하고, 너무 강력하기 때문에 그런것 같기도 하고,, 아리송 합니다 ! 같이 찾아보고 의논해보면 좋을 거 같아요!

dynamic_cast ⇒ 비용이 높기 때문에 (클래스의 이름 비교를 수행)

static_cast ⇒ 정보 손실, 런타임에 대한 정보를 알 수 없음

reinterpret_cast ⇒ 포인터 변환 문제?

  • 클래스 주소로 비교하지 않고 이름으로 비교하는 이유는 ???
  • class A ← class B ← class C
  •  

 

 

Q. static_cast이 위험한 cast일 수 있는 이유를 모두 말해보자

더보기

정보 손실

  • 부동 소수점 값을 정수로 static_cast한다고 가정할 때, 소숫점 이하의 값이 손실될 수 있다.

포인터 형 변환(역참조)

  • int 포인터를 double 포인터로 형 변환할 경우(역참조할 경우) 메모리 오류가 발생할 수 있음

가독성 저하

타입 안전성 감소

안전하지 않은 형 변환

  • static_cast는 컴파일 시간에 캐스팅을 진행하기 때문에 런타임 중 변경된 타입이 유효한지 검사하지 않기 때문
  • 만약 상위 클래스 포인터가 하위 클래스 객체를 가리키고 있지 않은 경우에 형 변환을 시도하면 런타임에 오류 발생
  • 다중 상속의 경우 두 클래스 간의 상속 구조가 복잡해질 수 있다
    • 예시코드
     

 

Q. 이 코드의 문제점은?

class Window{ 
public:
virtual void onResize() {...}
 ...
};
class Special Window :public Window{
public:
virtual void onResize(){
	static_cast<Window>(*this).onResize();
	}
};
더보기

가상 함수를 파생클래스에서 재정의해서 구현할 때 기본 클래스의 버전을 호출하는 문장을 가장 먼저 넣어달라는 요구사 항이 있을 때 케이스이다.
Window 객체로 타입캐스팅을 잘 해서 Window::onResize() 함수를 호출한 것으로 보이지만 실제로는 그렇지 않다. 타입 캐스팅이 될 때 *this 에 대한 사본이 임시적으로 만들어지게 되고 현재 객체가 아닌 사본 객체의 Window::onResize() 함수가 불리기 때문이다.
만약 Window::onResize() 가 객체를 수정하도록 만들어진 경우 현재 객체는 그 수정이 제대로 반영되지 않을 것이다.

 

수정한 코드

class Window {
public:
    virtual void onResize() { }
};
class SpecialWindow :public Window {
public:
    virtual void onResize() {
        // static_cast<Window>(*this).onResize();
        __super::onResize();
        // 또는 
        Window::onResize();
    }
};

 

타입 캐스팅을 하지 않고 Window::onResize() 함수를 부르도록 수정하여 해결이 가능하다.

 


 

 

 

 

항목 28:  내부에서 사용하는 객체에 대한 ‘핸들’을 반환하는 코드는 되도록 피하자

 

Q. 무효 참조 핸들에 대해 설명해보세요.

더보기

핸들이 있기는 하지만, 그 핸들을 따라갔을 때 실제 객체의 데이터가 없는 것을 의미합니다.
예를 들어, 핸들 불러왔는데 해당 객체가 이미 소멸됐을 때

danggling handler

 

 

Q. 핸들이란 무엇이고, 핸들을 사용할 때 생길 수 있는 문제점은 무엇일까요?

더보기

참고: 198~200쪽 

다른 객체에 손을 댈 수 있게 하는 매개자로, C++에서 참조자, 포인터, 반복자가 이에 해당됩니다. 

*자동차의 핸들처럼 나는 운전을 위해 필요한 엔진 어쩌구.. 이런게 뭔진 모르겠지만 아무튼 핸들만 알면 운전을 할 수 있다* 라고 멘토님께 설명을 들었었습니다!!

예를 들어 operator[] 연산자는 string이나 vector 등의 클래스에서 개개의 원소를 참조할 수 있게 만드는 용도로 제공되고 있는데, 실제로 이 연산자는 내부적으로 해당 컨테이너에 들어 있는 개개의 원소 데이터에 대한 참조자를 반환하는 식으로 (핸들을 반환하는 식으로) 동작합니다.

무효참조 핸들(dangling handle) 이라는 문제가 생길 수 있는데, 핸들이 있기는 하지만 그 핸들을 따라갔을 때 실제 객체의 데이터가 없는 것입니다. 이번 항목에서는 임시 객체 temp가 소멸되면서 나타나는 예시를 들어 설명하고 있습니다.

 

 

Q. 아래의 방법으로 객체를 반환받는다면 어떤 문제가 있을 수 있을까?

class Rectangle {...} // upperleft()함수 보유.
class GUIObject {...}

// Rectangle 객체를 생성하여 GUI객체의 사각 테두리 영역을 반환하는 함수
const Rectangle boundingBox(const GUIObject& obj);

GUIObject *pgo;
const Point *pUpperLeft = &(boundingBox(*pgo).upperleft());

 

더보기

예시의 문장이 끝날 때, 문장에서 임시로 생성된 Rectangle가 소멸합니다. 따라서 pUpperLeft가 가리키고 있는 객체는 없는 객체가 되어 무효참조 핸들(dangling Handle) 현상이 발생하게 됩니다.

 

 

Q. 아래의 밑줄 친 부분의 코드 다음줄에서 Rectangle의 임시 객체가 소멸되는것이 아닌가?

#include <iostream>
using namespace std;

class Point{
public:
	Point() { X = 0; Y = 0; }
	Point(int x, int y) : X(x),Y(y) {  }
	void SetX(int newVal) { X = newVal; }
	void SetY(int newVal) { Y = newVal; }
	int GetX() const { return X; }
	int GetY() const { return Y; }
private:
	int X;
	int Y;
};

struct RectData{
public:
	RectData(){  }
	virtual ~RectData(){  }
	Point ulhc; // 좌측 상단
	Point lrhc; // 우측 상단
};

class Rectangle{
public:
	Rectangle() 
	{
		pData = make_shared<RectData>();
	}
	Rectangle(Point coord1, Point coord2)
	{
		pData = make_shared<RectData>();
		pData->ulhc = coord1;
		pData->lrhc = coord2;
	}
	virtual ~Rectangle() {  }

private:
	shared_ptr<RectData> pData;
public:
	const Point& upperLeft() const { return pData->ulhc; }
	const Point& lowerRight() const { return pData->lrhc; }
};

class GUIObject{
public:
	GUIObject() {  }
	GUIObject(Point coord1, Point coord2)
	{
		rectangle = Rectangle(coord1, coord2);

	}
	virtual ~GUIObject() {  }
	const Rectangle& GetRectangle() const { return rectangle; }
private:
	Rectangle rectangle;
};

const Rectangle boundingBox(const GUIObject& obj)
{
	return obj.GetRectangle();
}

int main() {
	Point coord1(50, 50);
	Point coord2(100,100);

	const Rectangle rec(coord1, coord2);
	GUIObject* pgo1 = new GUIObject(coord1, coord2);
	const Point* pUpperLeft = &(**boundingBox(*pgo1)**.upperLeft());
	cout << pUpperLeft->GetX() << ' ' << pUpperLeft->GetY() << '\\n';

	Rectangle temp = boundingBox(*pgo1);
	const Point* pUpperLeft2 = &(temp.upperLeft());
	
	bool bk = false;
	delete pgo1;

	return 0; 
}
더보기

 

메모리 블록 중에 하나를 참조를 하고 있는데, 블록이 허용되었던 공간이었다가 허용되지 않은 공간이 되어서 문제가 생기는데, 바로 바로 사용을 할 때는 안보일 수도 있다.

문제가 생길 수 있는 여지가 있는 가능성이 크기 때문에, 사용하지 않는 것을 권장한다.


 

 

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

 

Q. 책에서 강력한 보장을 제공하기 위해서 사용한 방법 2가지는 무엇일까요?

더보기

Copy and Swap, 스마트 포인터

 

 

Q. 예외 안전성을 확보하기 위한 보장 3가지가 무엇이고 각각 어떤 보장을 뜻 할까요

더보기
  1. Basic Guarantee : 기본적인 보장어떤 객체나 자료구조도 더럽히지 않고, 모든 객체의 상태는 내부적으로 일관성을 유지하고 있다.
  2. 하지만, 프로그램의 상태가 어떤 지는 정확히 예측되지 않을 수 있다.
  3. 함수 동작 중 예외가 발생하면 실행 중인 프로그램에 관련된 모든 것을 유효한 상태로 유지하겠다는 보장이다.
  4. Strong Guarantee : 강력한 보장2가지 프로그램 상태만 존재한다. 예외가 발생하지 않으면 함수가 끝까지 동작하고, 예외가 발생하지 않으면 함수 호출이 없던 것처럼 되돌아 간다.
  5. 함수 동작 중 예외가 발생하면 프로그램의 상태를 절대로 변경하지 않겠다는 보장이다.
  6. Nothrow Guarantee : 예외 불가 보장기본제공 타인에 대한 모든 연산은 예외를 던지지 않게 되어 있어 예외불가 보장이 제공된다.
  7. 예외를 절대로 던지지 않겠다는 보장이다.

 

 

Q. copy and swap 에 대해 이야기를 나누어봅시다.

더보기

복사 후 맞바꾸기 방법은 객체의 데이터에 대한 사본을 만들어 놓고 그 사본을 변경한 후에 사본과 원본을 바꿔치기 작업을 예외를 던지지 않는 함수 내부에서 하자는 의미입니다.


효율 혹은 복잡성에서 생기는 비용이 있지만, 강력한 보장을 제공


 

 

 

 

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

 

Q. 메모리 공간이 적은 기계에서 인라인 함수를 남발할 시 문제점을 상의해보아요!

더보기

  • 프로그램 크기가 그 기계에서 쓸 수 있는 메모리 공간을 넘어버릴 수 있다.
  • 가상 메모리 환경에서 인라인 함수로 인해 부풀려진 코드는 성능의 걸림돌이 된다.
  • 페이징 횟수가 늘어나 명령 캐시 적중률이 떨어진다.
  • ⇒ 반대의 경우, inline 함수의 본문 길이가 매우 짧을 경우 목적코드의 크기도 작아지며 명령어 캐시 적중률이 높아짐.

 

 

Q. 인라인 함수의 제외 대상은 무엇일까요?

더보기

  • 컴파일러가 보기에 복잡한 함수 (루프가 들어 있다거나 재귀 함수인 경우)
  • 가상 함수 호출
  • ⇒ “virtual” : 어떤 함수를 호출할 지 결정을 프로그램 실행 중에 한다. 의미 (즉, 컴파일 시간에 일어나는 인라인 함수와 의미가 상반되기 때문임)
  • 함수 포인터를 통해 호출하는 경우
  • 생성자 및 소멸자
    • 객체 생성, 자동 초기화
    • 객체 소멸
    • 기본 클래스 부분과 객체의 데이터 멤버들이 자동으로 생성, 객체가 소멸될 대 반대 순서로 이루어짐
  • ⇒ C++는 객체가 생성되고 소멸될 때 일어나는 일들에 대해 여러가지 보장을 제공해주기 때문임

 

 

Q. inline과 forceinline의 차이는 무엇일까?

더보기

  • inline
    • 컴파일러에게 함수를 인라인으로 확장하도록 요청하는 지시자입니다. (함수 호출을 실제 함수의 코드로 대체하도록 권장하는 것).
  • forceinline
    • forceinline은 컴파일러에게 함수를 반드시 인라인으로 확장하도록 지시하는 지시자.컴파일러는 함수 호출을 무조건 해당 함수의 코드로 대체해야 함.
  • 공통된 특징
    • 주로 짧은 코드 또는 자주 호출되는 함수에 사용됩니다.
    • 함수가 너무 복잡하거나 크기가 너무 클 경우에는 인라인을 수행하지 않을 수 있음.

 

 

 

 

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

 

Q. 파일 사이의 컴파일 의존성을 최소화하는 방법에 대해 설명해보세요.

더보기

'정의부에 대한 의존성'을 '선언부에 대한 의존성'으로 바꿀 수 있다면 바꿉니다.   

  • 객체 참조자 똔는 포인터로 충분한 경우에는 객체를 직접 쓰지 않습니다.
    • 어떤 타입에 대한 참조자 및 포인터를 정의할때는 그 타입의 선언부만 필요합니다.  반면, 어떤 타입의 객체를 정의할 때는 그 타입의 정의가 준비되어 있어야 합니다.
  • 가능하다면 클래스 정의 대신 클래스 선언에 최대한 의존하도록 합니다.
    • 함수 선언이 되어 있는 (라이브러리의) 헤더 파일 쪽에 그 부담을 주지 않고 실제 함수 호출이 일어나는 사용자의 소스 파일 쪽에 전가한는 방법을 사용합니다.
  • 선언부와 정의부에 대해 별도의 헤더 파일을 제공합니다. 핸들클래스
    • 선언부를 위한 헤더 파일, 정의부를 위한 헤더 파일 2개를 만들어 사용합니다. 이 때 두 파일은 관리도 짝 단위로 해야 합니다. 

 

 

Q.  팩토리 함수(혹은 가상 생성자)에 대해 설명해보세요.

더보기

답.
팩토리 함수(Factory Function)는 객체를 생성하는 함수입니다. 일반적으로 객체 생성에 필요한 복잡한 로직이나 초기화를 감추는 데 사용됩니다. 주로 객체 생성을 담당하는 정적 메서드(static method)나 전역 함수로 구현됩니다.

 

인터페이스 클래스를 사용하기 위해서는 객체 생성 수단이 최소한 하나는 있어야 합니다. 이 때, 파생 클래스의 생성자 역할을 대신하는 팩토리 함수를 만들어 놓고 이 팩토리 함수를 호출하여 객체를 생성합니다. 

주어진 인터페이스 클래스의 인터페이스를 지원하는 객체를 동적으로 할당한 후, 그 객체의 포인터를 반환합니다.

 

 

팩토리 함수의 장점은 다음과 같습니다:

  • 객체 생성의 추상화: 팩토리 함수를 사용하면 클라이언트 코드에서 객체를 생성하는 방식을 추상화할 수 있습니다. 클라이언트 코드는 어떤 클래스의 객체를 생성하는지에 대해 몰라도 됩니다.
  • 유연한 객체 생성: 팩토리 함수를 사용하면 객체를 생성할 때 다양한 옵션을 고려할 수 있습니다. 예를 들어, 생성될 객체의 유형이 실행 시간에 결정되거나, 여러 가지 초기화 옵션이 있을 때 유용합니다.
  • 객체 생성 과정의 중앙 집중화: 팩토리 함수를 사용하면 객체 생성에 필요한 모든 코드를 중앙 집중화할 수 있습니다. 이로 인해 코드 중복이 줄어들고 유지보수가 쉬워집니다.

 

예를 들어, 게임에서 여러 유형의 캐릭터를 생성하는 경우, 각 캐릭터 유형에 대한 팩토리 함수를 만들어 사용할 수 있습니다. 클라이언트 코드는 캐릭터를 생성하기 위해 해당 팩토리 함수를 호출하면 됩니다. 이렇게 하면 클라이언트 코드가 각 캐릭터의 생성 방법에 대해 알 필요가 없어지고, 새로운 캐릭터 유형을 추가하거나 기존 캐릭터의 생성 방법을 변경하는 것이 쉬워집니다.

 

팩토리 메서드 패턴, 팩토리 패턴

https://jeonyeohun.tistory.com/385#:~:text=지금까지 정리한 내용,하게 하는 디자인 패턴이었습니다.

 

 

Q. 컴파일 의존성을 줄이는 방법 2가지를 말하고, 간단하게 설명해보아요

더보기

1. 핸들 클래스

  •  핸들 클래스는 클래스 구현부와 선언부를 분리하는 방법입니다.
  • 실제 필요한 구현을 분리함으로써 사용자는 내부에 대해 알지 않아도 되고, 컴파일 의존성을 떨어트려 놓을 수 있습니다.

2. 인터페이스 클래스

  • 인터페이스 클래스는 순수 가상함수로 이루어진 클래스 입니다.
  • 모두 가상함수로 이루어져있기 때문에, 가상함수테이블에 대한 비용이 든다는 단점을 가지고 있지만 컴파일 의존성을 내려놓을 수 있습니다.

 

 

Q. string 타입은 클래스 일까요?

X:  아니다. typedef으로 정의된 타입동의어이다.

O:  맞다. 클래스이다.

더보기

String은 클래스가 아니다. typedef로 정의된 타입동의어입니다. 그렇기때문에, 전방선언도 불가능합니다.

basic_string<char>를 typedef한 것입니다.

https://modoocode.com/234 

 


Q. 핸들 클래스와 인터페이스 클래스 사용시의 장단점은 무엇일까요?

더보기

((228쪽 참고))

[ 장점 ]

구현부로부터 인터페이스를 떼어 놓을 수 있어서 파일 간 컴파일 의존성을 완화시킨다.

 

[ 단점 ]

핸들 클래스의 단점

  • 멤버 함수를 호출하면 구현부 객체의 데이터까지 가기 위해 포인터를 타야 한다. (=간접화 연산이 한 단계 증가된다)
  • 객체 하나를 저장하는데 필요한 메모리 크기 + 구현부 포인터의 크기가 더해진다.
  • 구현부 포인터가 동적 할당된 구현부 객체를 가리키도록 (핸들 클래스의 생성자 안에서) 포인터 초기화가 일어나야 한다. 즉, 동적 메모리 할당과 해제에 따르는 연산 오버헤드가 발생한다.

 

인터페이스 클래스의 단점

  • 호출되는 함수가 가상 함수라서 함수 호출 시 가상 테이블 점프에 따르는 비용이 소모된다.

 

핸들, 인터페이스 모두의 단점

  • 핸들 클래스와 인터페이스 클래스 모두 함수 본문과 구현부를 분리하는 설계라서, 인라인 함수 호출이 불가하다(?)

 

 

< 인터페이스를 쓸 때 주의사항 >

  • C++은 C#/Java와 달리 시스템 상 인터페이스는 없고 개발자가 사용하는 방법입니다.  
  • 멤버변수를  선언하면 안 됩니다. 멤버변수를 선언하면 추상클래스가 되버려서 인터페이스 기능의 상실합니다.

 

※ 참고)  순수 가상 함수를 포함한 클래스를 인스턴스로 만들 수 없다. 그러므로 추상클래스와 인터페이스를 인스턴스로 만들 수 없다.