클래스에서 암시적 타입 변환을 지원하는 것은 일반적으로 못된 생각이다. 그러나 예외가 있는데, 가장 흔한 예외 중

하나가 숫자 타입을 만들 때이다. 예를 들어 유리수를 나타내는 클래스를 만들고 있다면, 정수에서 유리수로의 암시적

변환은 허용하더라도 잘못되었다고 볼 수 없다. 즉, int를 double 형으로 변환하는 것과 동일하다고 할 수 있다.

 

1
2
3
4
5
6
7
8
class Rational
{
public:
    Rational(int numerator = 0int denominator = 1); //암시적 변환을 허용하기 위해서 explicit을 붙이지 않았다.
 
    int numerator() const;        //분자.
    int denominator() const;    //분모.
};
cs

 

유리수를 나타내는 클래스인만큼 덧셈이나 곱셉 등의 수치 연산은 기본으로 지원하게 만들고 싶을텐데, 이럴 때는 어떤 식으로 지원해야

좋을지 고민해봐야 한다.

아마 흔히 생각나는 방법은 멤버 함수로 operator 연산자를 이용하여 구현하는 방법일 것이다.

 

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
class Rational
{
public:
    ...
 
    const Rational operator* (const Rational& rhs) const;
};
 
void Func()
{
    Rational oneEight(1,8);
    Rational oneHalf(1,2);
 
    Rational result = oneEight * oneHalf;    //이상 없음.
 
    result = result * oneEight;              //이상 없음.
}
cs

 

여기서 혼합형 수치 연산도 가능하길 원한다면 (Rational 객체를 int 같은 것과도 곱할수 있도록), 이것은 반쪽짜리 연산이었다는

사실을 알게 될 것이다. 하기 소스를 보자.

 

1
2
3
4
5
6
7
8
9
10
void Func()
{
    Rational oneEight(1,8);
    Rational oneHalf(1,2);
 
    Rational result = oneHalf * 2;   //이상 없음.
 
    result = 2 * oneHalf;            //컴파일 에러.
}
 
cs

 

곱셈은 기본적으로 교환법칙이 성립해야 되는데 상기에 있는 소스는 컴파일 에러가 발생한다.
상기 소스를 함수 형태로 바꾸어 써 보면 왜 컴파일 에러가 발생하는지 알 수 있다. 하기 소스 참조.

 

1
2
3
4
5
6
7
8
9
10
11
void Func()
{
    Rational oneEight(1,8);
    Rational oneHalf(1,2);
 
    Rational result = oneHalf * 2;    
    // => Rational result = oneHalf.operator*(2);
 
    result = 2 * oneHalf;
    // => result = 2.operator*(oneHalf);
}
cs

 

첫 번째 result 연산에서 oneHalf 객체는 operator* 연산자를 멤버로 갖고 있는 클래스의 인스턴스이므로,
컴파일러는 이 함수를 문제없이 호출한다. 하지만 두 번째 result 연산에서 정수 2에는 클래스 같은 것이 아니기 때문에,

operator* 연산자가 있을 리가 없다. 그럼 컴파일러는 호출할 수 있는 비멤버 operator*(네임스페이스 또는 전역 유효범위에

있는 operator*) 연산자를 찾아본다.

 

result = operator*(2, oneHalf); // 컴파일 에러.

 

그러나 상기 소스와 같이 int형과 Rational을 인자로 갖는 비멤버 버전의 operator* 연산자가 없으므로 컴파일 에러가 발생한다.

 

그럼, 상기 소스에서 Rational result = oneHalf.operator*(2);는 문제가 발생하지 않은 이유에 대해서 알아보자.
이유는 바로 암시적 타입 변환이다.

컴파일러는 함수 쪽에선 Rational 타입을 요구하지만 프로그래머가 operator* 함수에 int를 넘겼다는 사실을 알고 있으나,

이 int를 Rational 클래스의 생성자에 주어 호출하면 Ratinal로 둔갑시킬 수 있다는 사실 또한 알고 있다.

즉, 컴파일러는 하기와 같은 소스로 처리한다는 것이다.

 

1
2
3
4
5
6
    Rational result = oneHalf * 2;    
    // => Rational result = oneHalf.operator*(2);
 
    //result = oneHalf * 2; 상기 문장을 컴파일러는 하기와 같이 처리한다.
    const Rational temp(2);            //2로부터 임시 Rational 객체를 생성.
    Rational result = oneHalf * temp; // oneHalf.operator*(temp); 와 같은 문장.
cs

 

물론 컴파일러가 이렇게 동작하는 것은 명시호출(explicit)로 선언되지 않은 생성자가 있기 때문이다.

만약 Rational 생성자가 명시호출 생성자였으면 하기 코드 모두 컴파일 에러가 발생한다.

 

1
2
3
    Rational result = oneHalf * 2;   //컴파일 에러.
 
    result = 2 * oneHalf;            //컴파일 에러.
cs

 

 

그럼 이제 혼합형 수치 연산이 문제 없도록 만들어보자.

우선 여태까지 알아봤던 결론으로는 암시적 타입 변환에 대해 매개변수가 먹혀들려면 매개변수 리스트에 들어 있어야만 한다는

것이다. 그러니까 멤버 함수를 호출하는 객체에 해당하는 암시적 매개변수는 암시적 변환이 먹히지 않는다는 것이다.

(즉, 2 가 operator* 멤버 함수를 호출하는 객체이므로 이렇게 멤버 함수를 호출하는 쪽의 객체는 암시적 형변환이 되지 않는다는

 것이다.)

 

결국 이를 해결하기 위해서는 2가 멤버 함수를 호출하지 않는 방향으로 가야하므로 operator* 함수를 멤버 함수가 아닌 비멤버 함수로

선언하여 사용하면 된다.

 

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
class Rational
{
private:
    int m_numerator;
    int m_denominator;
 
public:
    Rational(int numerator = 0int denominator = 1); //암시적 변환을 허용하기 위해서 explicit을 붙이지 않았다.
 
    int numerator() const;        //분자.
    int denominator() const;      //분모.
};
 
const Rational operator*(const Rational& lhs, const Rational& rhs)
{
    return Rational((lhs.numerator() * rhs.numerator()), (lhs.denominator() * rhs.denominator()));
}
 
void Func()
{
    Rational oneEight(1,8);
    Rational oneHalf(1,2);
 
    Rational result = oneHalf * 2;
    // => Rational result = operator*(oneHalf, temp(2));  정상 동작.
 
    result = 2 * oneHalf;
    // => result = operator*(temp(2), oneHalf);           정상 동작.
}
cs

 

여기서 한 가지 더 고민해볼 것이 있다. Operator* 함수를 Rational 클래스의 프랜드 함수로 두어도 되는가?에 대한 질문이다.

그리고 이 예제에서의 대답은 '아니오'이다.

왜냐하면 이 예제에서 operator* 연산자는 완전히 Rational의 public 인터페이스만을 써서 구현할 수 있기 때문이다.
여기서 한 가지 중요한 결론을 뽑을 수 있는데.. 멤버 함수로 하면 안되는 함수에 대해서 프랜드로 만든다고 해결되는 것이 아니라는 점이다.
즉, "멤버 함수이면 안 되니까"라는 말이 반드시 "프랜드 함수이어야 해"라는 말이 아니라는 것이다.
멤버 함수의 반대는 프랜드 함수가 아니라 비멤버 함수이다.

 

1줄 요약

 

 - 어떤 함수에 들어가는 모든 매개변수(this 포인터가 가리키는 객체도 포함)에 대해 암시적 타입 변환을 해 줄 필요가 있다면,

   그 함수는 비멤버 함수이어야 한다.

어떤 클래스를 만들면 이런 저런 함수를 통해 제공하는 기능이 많을 것이다.

하기 내용은 클래스 설계자보다는 클래스를 사용하여 프로그래밍을 하는 사용자 입장에서 클래스를 사용하는데 있어

편하게 확장시킬 수 있는 방법에 대한 내용이라고 생각하면 조금 더 쉽게 생각할 수 있다.

 

1
2
3
4
5
6
7
8
9
class WebBrowser
{
public:
    ...
    void clearCache();
    void clearHistory();
    void removeCookies();
    ...
};
cs

 

위와 같이 동작하는 함수들을 한 번에 하고 싶을 때도 있기 때문에, 세 함수를 모아서 호출하는 함수도 준비해 둘 수 있다.

 

<멤버 함수 ver>

1
2
3
4
5
6
7
8
9
10
11
class WebBrowser
{
public:
    ...
    void clearCache();
    void clearHistory();
    void removeCookies();
 
    void clearEverything();    //상기 3개 함수 호출.
    ...
};
cs

 

<비멤버 함수 ver>

1
2
3
4
5
6
void clearBrowser(WebBrowser& wb)
{
    wb.clearCache();
    wb.clearHistory();
    wb.removeCookies();
}
cs

 

멤버 버전인 clearEverything과 비멤버 버전인 clearBrowser 중 어떤 쪽이 더 좋은지 살펴보자.
객체 지향 법칙에 관련된 이야기를 찾아보면 데이터와 그 데이터를 기반으로 동작하는 함수는 한 데 묶여 있어야하며,
멤버 함수가 더 낫다고들 한다. 그러나 이는 잘 못된 지식이다. 분명히 객체 지향 법칙은 할 수 있는 만큼 데이터를 캡슐화하라고

주장하지만 멤버 버전인 clearEverything은 비멤버 버전인 clearBrowser보다 캡슐화 정도가 오히려 형편없다.

또한 비멤버 함수를 사용하면 WebBrowser 관련 기능을 구성하는데 있어서 패키징 유연성이 높아지는 장점도 있고 컴파일

의존도도 낮추어 WebBrowser의 확장성을 높일 수 있다.

 

우선 캡슐화하는 것이 늘어나면 그만큼 밖에서 볼 수 있는 것들이 줄어들기 때문에, 내부적으로 변경될 때 필요한 유연성이 커진다.

즉, 이미 있는 코드를 바꾸더라도 제한된 사용자들밖에 영향을 주지 않는 융통성을 확보할 수 있다는 것이다.

정리하면... 앞에서도 말 했듯이 멤버 변수에 접근할 수 있는 함수의 개수가 적을수록 캡슐화가 잘 되었다고 할 수 있다.

그러나 멤버 함수가 늘어나면 늘어날 수록 멤버 변수에 접근할 수 있는 함수의 개수도 늘어나게 되는 것이므로 똑같은 기능을

제공하는데 멤버 함수를 쓰는 것보다는 비멤버 비프랜드 함수를 사용하는 것이 더 바람직하다고 볼 수 있다.

비멤버 비프른드 함수는 어떤 클래스의 private 멤버 부분을 접근할 수 있는 함수의 개수를 늘리지 않기 때문이다.

 

좋은 방법

 

C++에서는 Webrowser가 가진 private 멤버의 캡슐화에 영향을 주지 않으면서 자연스런 방법으로 구사할 수 있다.

clearBrowser를 비멤버 함수로 두되, WebBrowserstuff와 같은 네임스페이스 안에 두는 것이다.

 

1
2
3
4
5
6
7
8
namespace WebBrowserStuff
{
    class WebBrowser { ... };
 
    void clearBrowser(WebBrowser& wb);
 
    ...
}
cs

 

이 방법은 매우 진보적인 방법이라고 볼 수 있다. 네임스페이스는 클래스와 달리 여러 개의 소스 파일에 나뉘어 흩어질 수 있기 때문이다.

clearBrowser 같은 함수들은 편의상 준비한 함수들이며 멤버도 프랜드도 아니기에, WebBrowser 사용자 수준에서 아무리 애를 써도

얻어낼 수 없는 기능은 이들도 제공할 수 없다. 예를 들어, clearBrowser가 없다면 사용자는 그냥 clearCache, clearHistory, removeCookies

를 호출하면 그만이다.

 

WebBrowser처럼 응용도가 높은 클래스는 이런 종류의 편의 함수가 꽤 많이 생길 수 있다. 즐겨찾기에 관련된 함수라든지, 인쇄에 관련된

함수도 있을 것이고, 쿠키 관리용 함수 등등이 있을 것이다.

이것들을 나누어 놓는 쉽고 깔금한 방법은, 즐겨찾기 관련 편의 함수를 하나의 헤더 파일에 몰아서 선언하고, 쿠키 관련 편의 함수는 다른

헤더 파일에 몰아서 선언하고, 인쇄 관련 편의 함수는 또 다른 헤더 파일에 몰아서 선언하는 것이다. <하기 참조>

 

<WebBrowser.h>

1
2
3
4
5
6
namespace WebBrowserStuff
{
    class WebBrowser { ... };
 
    ...
}
cs

 

<WebBrowserClear.h>

1
2
3
4
namespace WebBrowserStuff
{
    void clearBrowser(WebBrowser& wb); //clear 관련 편의 함수
}
cs

 

<WebBrowserBookmark.h>

1
2
3
4
namespace WebBrowserStuff
{
    //즐겨찾기 관련 관련 편의 함수들이 여기에 들어간다.
}
cs

 

<WebBrowserCookies.h>

1
2
3
4
namespace WebBrowserStuff
{
    //쿠키 관련 관련 편의 함수들이 여기에 들어간다.
}
cs

 

표준 C++ 라이브러리가 이러한 구조로 구성되어 있다. std 네임스페이스에 속한 모든 것들이 <C++ standardLibrary> 헤더 같은

것에 모조리 들어가 있지 않고, 몇 개의 기능과 관련된 함수들이 수십 개의 헤더(<vector>, <algorithm>, <memory> 등등..)에

흩어져 선언되어 있다.

 

이렇게 사용하면 vector 기능만 필요한 사용자는 굳이 <memory>를 #include 할 필요가 없으며 사용자가 필요한 기능에 대해서만

#include를 이용하여 사용하면 된다. 결국 사용자가 실제로 사용하는 구성요소에 대해서만 컴파일 의존성을 고려할 수 있게 된다.

그러나 멤버 함수로 사용하게 되면 이런 식으로 기능을 쪼개는 것이 불가능하므로 하나의 클래스는 그 전체가 통으로 정의되어야만

한다.

 

또한 여러 개의 헤더 파일에 나누어 놓으면 편의 함수 집합의 확장도 쉽게 할 수 있다.

만약 사용하던 중에 사용자가 추가로 다운로드 이미지에 관련된 편의 함수가 필요하다고 생각된다면 사용자가 직접 헤더 파일을 하나

추가해서 WebBrowserStuff 네임스페이스를 만들고 그 안에 관련 함수들을 집어넣기만 하면 되기 때문이다.

이런 부분 역시 클래스 멤버 함수였다면 사용자가 클래스 정의 자체를 수정할 수 없기 때문에 불가능하였을 것이다.

만약 해당 클래스를 상속받아서 사용자가 사용한다고 해도 파생클래스는 기본 클래스 안에 캡슐화된 멤버에 대한 접근권한이 없기

때문에, 이런 식의 확장은 좋은 방법이 아니다.

 

1줄 요약

 

 - 멤버 함수보다는 비멤버 비프렌드 함수를 자주 쓰도록 하자. 캡슐화 정도가 높아지고, 패키징 유연성도 커지며, 기능적인

   확장도 늘어난다.

모든 멤버 변수는 private으로 하는 것이 좋다. 상속 관계에서도 동일하다.

우선 모든 멤버 변수를 private으로 선언하면 문법적 일관성을 유지하는데 좋다.

사용자는 고민없이 무조건 멤버 변수에 접근하기 위해서 괄호를 붙여 접근하면 된다.

어떤 변수는 변수명으로 접근해야 하고 어떤 변수는 함수를 통해 접근해야된다면 사용자 입장에서는 헷갈릴만한 문제이다.

 

또한 해당 private 변수에 접근 권한을 부여할 수 있기 때문에 정교하게 제어할 수 있다. (하기 코드 참조)

 

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
class AccessLevels
{
public:
    int getReadOnly() const         { return nReadOnly; } 
 
    void setReadWrite(int value) { nReadWrite = value; }
    int getReadWrite()             { return nReadWrite; }
 
    void setWriteOnly(int value) { nWriteOnly = value; }
 
private:
    int nNoAccess;    //해당 int에는 접근 불가.
 
    int nReadOnly;  //해당 int에는 읽기 전용 접근 가능.
 
    int nReadWrite; //해당 int에는 읽기 쓰기 접근 가능.
 
    int nWriteOnly; //해당 int에는 쓰기 전용 전급 가능.
};
cs

 

이렇게 세밀한 접근 제어는 나름대로의 중요성을 갖고 있다. 어떤 식으로든 외부에 노출시키면 안되는 데이터 멤버들이

꽤 많기 때문이다. 또한 캡슐화를 보다 견고하게 할 수 있다.

 

다른 예를 보자. 하기는 자동차의 속도 값을 모아서 평균을 반환하는 클래스이다.

 

1
2
3
4
5
6
class SpeedDataCollection
{
public:
    void addValue(int speed);    //새 데이터 값 추가.
    double averageSoFar() const//평균 속도 반환.
};
cs

 

averageSoFar 멤버 함수를 구현하는 방법은 2가지가 있다.

 

첫 번째 방법.

 

 -지금까지 수집한 속도 데이터 전체의 평균값을 담는 어떤 데이터 멤버를 클래스 안에 넣어두는 방법.

  averageSofar 함수를 호출할 때마다 이 데이터 멤버의 값을 반환한다.

 

두 번째 방법.

 

 - averageSoFar 함수가 호출될 때마다 평균값을 계산해서 반환하는 방법.

 

첫 번째 방법을 사용하면 SpeedDataCollection 객체의 크가 커지지만(평균값을 유지하기 위한 메모리들이 추가 되기 때문)

averageSoFar 함수의 효율은 좋을 것이다.

두 번재 방법을 사용하면 함수 자체의 속도는 느려지지만, SpeedDataCollection 객체의 크기는 첫 번째보다 작아질 것이다.

 

상황에 따라 어떤 방법을 쓰든 상과없지만 중요한 포인트는 두 방법 모두 "평균값 접근에 멤버 함수를 통하게 한다. (다른 말로

평균값을 캡슐화한다.)"라는 점이다.

 

사용자로부터 데이터 멤버를 숨기면(캡슐화하면), 클래스의 불변속성을 항상 유지하는데 절대로 소홀해질 수 없게 된다.

불변속성을 보여줄 수 있는 통로가 멤버 함수밖에 없기 때문이다. 또한 캡슐화는 현재의 구현을 나중에 바꾸기로 결정할 수 있는

권한을 예약하는 셈이다.

만약 멤버 변수가 public으로 되어있으면 사용자는 이 멤버 변수를 직접 가져다 사용했을텐데... 이 멤버 변수를 수정해야되는 일이

발생하면, 사용자 코드도 죄다 수정을 해줘야하기 때문에 수정 작업에 어려움을 겪게 된다.

그러므로 public 이란 "캡슐화되지 않았다"는 뜻이며 또한 "바꿀 수 없다"라는 의미를 담게 되는 것이다.

 

private은 멤버 함수로 접근하기 때문에 내부적으로 멤버 변수가 바껴도 사용자 코드에서는 수정할 부분이 없다는 얘기이다.

즉, 위의 코드에서 평균을 반환하는 방법이 내부적으로 바뀌더라도 사용자는 averageSoFar 함수를 통해 얻는 Data는 동일하기 때문에

아무런 문제가 되지 않는다는 것이다.

 

protected 또한 public과 다를바가 없다. 외부에서 접근을 못하는 것을 제외하면 캡슐화 측면에서는 public과 동일하다.

상속 관계에서는 멤버 변수를 그대로 가져다 사용할 수 있기 때문에 부모 클래스의 멤버 변수가 수정되면 자식 클래스에 엮여있는

해당 변수들도 전부 수정 작업이 되어야 하기 때문이다.

 

캡슐화의 관점에서 접근 수준은 private와 private가 아닌 나머지 이렇게 둘 뿐이다.

 

2줄 요약

 

 - 데이터 멤버는 private 멤버로 선언하자. 클래스 제작자는 문법적으로 일관성 있는 데이터 접근 통로를 제공할 수 있고,

   세밀한 접근 제어도 가능하며, 클래스의 불변속성을 강화할 수도 있고, 내부 구현의 융통성도 발휘 할 수 있다.

 

 - protected는 public과 똑같다고 생각하자.

 

 

 

 

앞 장에서 배운 내용으로 모든 코드를 '참조에 의한 전달'만으로 구성하려고 할 것이다.
그러나 함수 반환의 경우에는 참조자를 넘기는 것은 좋지 않다.

 

유리수를 나타내는 클래스가 하나 있고 두 유리수를 곱하는 멤버 함수가 있다고 가정하자.

 

1
2
3
4
5
6
7
8
9
10
11
class Rational
{
public:
    Rational(int numerator = 0int denominator = 1//분자, 분모
 
private:
    int n,d; 
 
friend
    const Rational& operator*(const Rational& lhs, const Rational& rhs); //두 곱셈 결과를 반환.
};
cs

 

배운대로 '참조에 의한 전달'을 했지만 생각해보면 위의 코드는 이상한 코드이다.
참조자는 반드시 이미 존재하는 Rational 객체의 참조자여야 한다, 그렇다면 대체 반환할 때 이미 존재하는 어떤 Rational 객체를

반환해야 된다는 것인가??

 

1
2
3
4
5
6
7
void Func()
{
    Rational a(1,2);   // a = 1/2
    Rational b(3,5);   // b = 3/5
 
    Rational c = a*b;  // c = 3/10이 될 것이다. 
}
cs

 

위의 코드를 보면 a*b위의 결과를 바로 c의 객체에다가 집어넣는다.
a*b의 결과 값을 가지는 Rational 객체가 존재해야지만 참조형으로 반환을 해줄 수 있을텐데 a*b의 결과인 임시 객체에 참조를

한다는 것은 아무런 의미가 없는 행동이다.

 

잘못된 시도1. 지역 객체를 만들어서 반환해보기.

 

아래와 같은 미친짓은 하지 말자.

 

1
2
3
4
5
const Rational& operator*(const Rational& lhs, const Rational& rhs)
{
    Rational result(lhs.n*rhs.n. lhs.n*rhs.n);
    return result;    //미친짓
}
cs

 

위와 같은 경우는 지역 객체이고 operator* 함수가 끝나면 result 객체도 자연히 소멸되는데 저 소멸되는 객체의 참조자를

반환하는 것은 절대 하지 말아야 한다.

또한 애시당초 참조자를 전달하는 목적은 생성자를 호출하지 않기 위함인데 위와 같은 코드는 어차피 생성자도 호출되기 때문에

'참조에 의한 전달'이 의미가 없게 된다.

 

잘못된 시도2. new로 힙에 메모리를 생성해서 반환하기.

 

1
2
3
4
5
const Rational& operator*(const Rational& lhs, const Rational& rhs)
{
    Rational* result = new Rational(lhs.n*rhs.n. lhs.n*rhs.n);
    return result;    //이것도 미친짓
}
cs

 

다른 방법으로 힙에 메모리를 생성하여 소멸되지 않도록 해보려고도 할 것이다.

그러나 어차피 이 방법도 생성자가 호출되며 더 큰 문제는 해당 객체를 누가 delete를 해줄 것인지에 대한 문제이다.

만약 위와 같은 코드를 만든 상태에서 아래와 같이 함수가 호출되었다고 가정하자.

 

1
2
3
4
5
6
void Func2()
{
    Rational w. x. y. z;
 
    w = x * y * z; //operator*(operator*(x,y), z)와 같다.
}
cs

 

위의 x * y * z; 문장에서 operator*가 2번 호출되어 new가 2번 실행했으니 delete 또한 2번 해줘야 하는데....

프로그래머는 위의 객체에 접근할 방법이 없기 때문에 delete를 해줄수가 없다.

 

잘못된 시도2. 정적 객체를 만들어 정적 객체를 반환하기.

 

그래서 마지막으로 한 번 더 시도하게 되는 것이.... static일 것이다.

 

1
2
3
4
5
6
7
8
const Rational& operator*(const Rational& lhs, const Rational& rhs)
{
    static Rational result;
 
    result = ...;
 
    return result;    //가장 미친짓
}
cs

 

위의 코드는 스레드 안정성 문제가 얽혀 있다.
또한 만약 아래와 같은 코드가 진행된다면 정상적으로 진행되지 않을 것이다.

 

1
2
3
4
5
6
7
8
9
10
11
void Func2()
{
    Rational a. b. c. d;
 
    ...
 
    if((a*b) == (c*d))
    {
        //두 유리수 곱이 같으면 어떤 처리를 한다.
    }
}
cs

 

위의 operator== 부분 코드를 바꿔보면 아래와 같다.

 

1
if (operator==(operator*(a,b), operator*(c,d)));
cs

 

a,b,c,d에 적절한 값이 들어가고 2번의 operator* 연산이 진행되어 정적 객체가 각 각 반환되었을 것이다.

그리고 operator== 함수가 호출되어 두 개의 값이 같은 비교를 할 것이다.

그런데 a,b,c,d 값이 무엇이 되었든 간에 정적 Rational 객체의 값이 반환되므로 무조건 마지막에 계산된 operator*의

반환 값이 Rational 객체의 값으로 설정되어 있을 것이다.

 

즉, a,b,c,d 값에 상관없이 operator* 의 반환 값은 항상 일치하므로 상기의 조건문은 무조건 true가 된다.

 

해결 방법

 

위의 잘못된 시도들에서 봤듯이 무조건 '참조에 의한 전달'만이 옳은 것은 아니다.

그러므로 아래와 같이 새로운 객체를 반환하게 만들어 반환하도록 하자.

 

1
2
3
4
const Rational operator*(const Rational& lhs, const Rational& rhs)
{
    return Rational(lhs.n * rhs.n, lhs.d * rhs.d);    //가장 미친짓
}
cs

 

이 방법 또한 새로운 객체가 생성되는 것이므로 생성자와 소멸자가 호출된다.

그러나 위와 같이 에러를 유발하게 되는 위험 부담은 일체 없다. 컴파일러 구현자들이 가시적인 동작 변경을 가하지 않고도

기존 코드의 수행 성능을 높이는 최적화를 적용할 수 있도록 해두었다. 그러므로 상기 동작은 생각보다 빠르다.

 

참조자를 반환할지 새로운 객체를 반환할 것인가를 결정할 때, 올바른 동작이 이루어지도록 만들어야 한다는 것은 반드시

명심해야 한다.

 

1줄 요약

 

 - 지역 스택 객체에 대한 포인터나 참조자를 반환하는 일, 힙에 할당된 객체에 대한 참조자를 반환하는 일 또는

   지역 정적 객체에 대한 포인터나 참조자를 반환하는 일은 그런 객체가 두 개 이상 필요해질 가능성이 있다면

   절대로 하지 마라.

C++는 함수로부터 객체를 전달받거나 함수에 객체를 전달할 때 '값에 의한 전달' 방식을 사용한다. (C에서 물려받은 특성)

값에 의한 전달 방식을 사용하면, 함수 매개변수는 실제 인자의 '복사본'을 통해 초기화되며, 함수가 반환할 때 함수를 호출한

쪽은 함수가 반환한 값의 '복사본'을 돌려받는다.

 

복사본을 만들어내는 원천이 바로 복사 생성자이므로 '값에 의한 전달'이 고비용 연산이 되기도 한다.
왜 그런지 아래 예제로 확인해보자.

 

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
class Person
{
public:
    Person();
    virtual ~Person();
 
private:
    std::string strName;
    std::string strAddress;
};
 
class Student : public Person
{
public:
    Student();
    ~Student();
 
private:
    std::string strSchoolName;
    std::string strSchoolAddress;
};
 
 
bool ValidateStudent(Student s); //유효한 학생 정보인지 체크하는 함수
 
void Func()
{
    Student plato;
 
    bool platoIsOK = ValidateStudent(plato); //plato 객체 체크.
}
cs

 

plato로부터 매개변수 s를 초기화시키기 위해 Student의 복사 생성자가 호출된다.
그리고 s는 ValidateStudent 함수가 종료되면 소멸된다.
즉, ValidateStudent가 호출되면 Student의 복사 생성자가 한 번 호출되고 Student의 소멸자가 한 번 호출된다.

 

그러나 Student에는 string 객체 2개가 있기 때문에, Student 객체가 생성될 때마다 멤버 객체들도 덩달아 생성되어야 한다.
게다가 부모클래스인 Person객체도 생성되어야 하므로 Person 객체의 string 객체 2개 또한 생성된다.

Student 객체를 값으로 전달하면, Student 복사 생성자 호출 1번, Person 복사 생성자 호출 1번, string 복사 생성자 호출

4번이 일어난다. 그리고 Student 객체의 사본이 소멸될 때도 앞에서 호출된 생성자 호출 횟수만큼 소멸자도 호출된다.
결과적으로 총 생성자 6번, 소멸자 6번이 호출되는 것이다.

 

이런 불필요한 과정을 거치지 않도록 하기 위해서 매개변수를 넘길 때 상수객체에 대한 참조자로 전달하면 된다.

 

1
2
3
4
5
6
7
8
bool ValidateStudent(const Student& s); //유효한 학생 정보인지 체크하는 함수
 
void Func()
{
    Student plato;
 
    bool platoIsOK = ValidateStudent(plato); //plato 객체 체크.
}
cs

 

이렇게 하면 새로 만들어지는 객체같은 것이 없기 때문에, 생성자와 소멸자가 호출되지 않는다.
그리고 원래 값에 의한 전달은 사본으로 작업이 되기 때문에 원본에는 아무런 영향이 없었지만 참조자를 전달하였으므로

작업 도중 원본에 영향을 미칠 수 있다. 이러한 문제를 방지하기 위해서 const를 붙여주는 것이다.

 

또한 참조자로 매개변수를 넘기면 파라미터로 넘어온 데이터가 짤리는 문제(복사 손실, slice problem)도 방지할 수 있다.
아래 예처럼 함수의 파라미터는 부모 클래스형이고 함수 호출부의 파라미터는 자식 클래스 형인 경우를 보자.

 

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
class Window
{
public:
    ...
    std::string name() const;     //해당 윈도우 이름을 반환해주는 함수.
    virtual void display() const//일반적인 윈도우를 화면에 그려주는 함수.
};
 
class windowWithScrollBars : public Window
{
public:
    ...
    virtual void display() const//스크롤바 기능이 있는 윈도우를 그려주는 함수.
};
 
void printNameAndDisplay(Window w) //함수의 파라미터는 부모클래스 형.
                                   //값에 의한 전달이므로 자식클래스부분은 싹 짤림. (slice)
{
    std::cout << w.name();
    w.display();
}
 
void Func()
{
    windowWithScrollBars wBar;
 
    printNameAndDisplay(wBar);        //함수 호출부의 파라미터는 자식클래스 형
}
cs

 

위의 예제처럼 값에 의한 전달을 하는 경우에는 매개변수 w가 생성되기는 하는데 부모클래스에 대한(Window) 데이터만

생성된다.

즉, 자식클래스 부분은 생성되지 않고 짤리게 되며 여기서w.Display()를 호출하면 Window 클래스의 Display 함수가 호출된다.

 

1
2
3
4
5
6
7
8
9
10
11
12
13
void printNameAndDisplay(const Window& w) //함수의 파라미터는 부모클래스 형.
                                          //참조에 의한 전달이므로 자식클래스 부분도 전달됨.
{
    std::cout << w.name();
    w.display();
}
 
void Func()
{
    windowWithScrollBars wBar;
 
    printNameAndDisplay(wBar);        //함수 호출부의 파라미터는 자식클래스 형
}
cs

 

이제 w는 어떤 타입의 윈도우 객체가 넘겨지더라도 넘겨진 객체 자체의 성질을 그대로 갖게 된다.

 

참조자는 보통 포인터를 써서 구현된다. 즉, 참조자를 전달한다는 것은 결국 포인터를 전달하는 것과 같은 것이다.
그러므로 기본 제공 타입(int 등..)일 경우에는 그냥 값에 의한 전달을 해도 상관이 없다.

그리고 한 가지 주의할 점은 객체 크기가 작고 멤버 변수 얼마 없다고 해서 '값에 의한 전달'을 선택해서는 안된다.

이유는 컴파일러 중에는 기본제공 타입과 사용자 정의 타입을 아예 다르게 취급하는 것들이 있기 때문이다.
진짜 int나 double은 레지스터에 넣어주지만, double 하나로만 만들어진 사용자 정의 타입은 레지스터에 넣지 않는다.
그러므로 사용자 정의 타입은 참조에 의한 전달을 쓰는 편이 좋다. 포인터(참조자도 포인터로 구현됨)만큼은 레지스터에

들어가기 때문이다.

 

'값에 의한 전달'이 저비용이라고 가정해도 괜찮은 타입은 기본 제공 타입, STL 반복자, 함수 객체 타입 이렇게 세 가지 뿐이다.
이외에는 무조건 '참조에 의한 전달'을 사용하자.

 

2줄 요약

 

 - '값에 의한 전달'보다는 '상수객체 참조자에 의한 전달'을 사용하자. 성능뿐만 아니라 복사손실 문제까지도 막아준다.

 - 기본제공 타입, STL 반복자, 함수 객체 타입에만 값에 의한 전달을 사용하자.

C++에서 새로운 클래스를 정의한다는 것은 새로운 타입을 하나 정의하는 것과 같다.
하기 항목들은 클래스 설계 시 고려해야 되는 항목들이다.

 

 - 새로 정의한 타입의 객체 생성 및 소멸은 어떻게 이루어져야 하는가?

 

 - 객체 초기화는 객체 대입과 어떻게 달라야 하는가?

   생성자와 대입 연산자의 동작 및 둘 사이의 차이점을 결정짓는 요소이다.
   초기화와 대입을 헷갈리지 않는 것이 가장 중요하다.

 

 - 새로운 타입으로 만든 객체가 값에 의해 전달되는 경우에 어떤 의미를 줄 것인가?

 

   어떤 타입에 대해 '값에 의한 전달'을 구현하는 쪽은 복사 생성자이다.

 

 - 새로운 타입이 가질 수 있는 적법한 값에 대한 제약은 무엇으로 잡을 것인가?

 

 - 기존의 클래스 상속계통망에 맞출 것인가?
 

   이미 가지고 있는 클래스로부터 상속을 시킨다면, 이들 클래스에 의해 제약을 받는다. 특히 멤버 함수가 가상인지 비가상인가의

   여부가 가장 큰 요인이다.

   내가 만든 클래스를 다른 클래스들이 상속할 수 있게 만들자고 결정했다면, 멤버 함수의 가상 함수 여부가 결정된다.

   특히 소멸자가 이런 경우에 해당한다.

 

 - 어떤 종류의 타입 변환을 허용할 것인가?

 

   타입 변환이 되도록 하고 싶다면 암시적/명시적 타입 변환을 선택해서 만들어야 한다.

 

 - 어떤 연산자와 함수를 두어야 의미가 있을까?


   어떤 것들이 멤버 함수로 적당할 것이고, 또 몇몇은 그렇지 않을 것이다. (항목 23,24,46참조)

 

 - 표준 함수들 중 어떤 것을 허용하지 말 것인가?


   어떤 함수를 private으로 선언해야 하는지 생각할 것.

 

- 새로운 타입의 멤버에 대한 접근권한을 어느 쪽에 줄 것인가?

 

   어떤 클래스 멤버를 public, private, protected 영여겡 둘 것인가를 결정하는데 도움을 주게 될 질문이다.

   또한 프렌드로 만들어야 할 클래스 및 함수를 정하는 것은 물론이고 한 클래스를 다른 클래스에 중첩시켜도 되가는가에 대한

   결정을 내리는 데도 도움을 줄 질문이다.

 

 - 선언되지 않은 인터페이스로 무엇을 둘 것인가?

 

 - 새로 만드는 타입이 얼마나 일반적인가?

 

   타입 하나를 정의하는 것이 아닐지도 모른다.
   설계된 클래스는 동일 계열의 타입군 전체일지도 모르기 때문에 클래스 템플릿을 정의해야할지도 모른다.

 

 - 정말로 꼭 필요한 타입인가?

 

   기존의 클래스에 대해 기능 몇 개가 아쉬워서 파생 클래스를 새로 뽑고 있다면, 차라리 간단하게 비멤버 함수라든지 템플릿을

   몇 개 더 정의하는 편이 더 좋다.

 

위에 질문들은 만만하게 볼 수 없는 질문들이다. 그래서 효과적인 클래스를 정의하는 일이 무척이나 어렵다는 말이 나오는 것이다.

 

1줄 요약

 

 - 클래스 설계는 타입 설계이다. 새로운 타입을 정의하기 전에, 이번 항목에 나온 모든 고려사항을 빠짐없이 점검해 보자.

제대로 쓰기엔 쉽고 엉터리로 쓰기에 어려운 인터페이스를 개발하려면 우선 사용자가 저지를 만한 실수의

종류를 머리에 넣어두고 있어야 한다.

 

예시 1

 

아래 예는 날짜를 나타내는 어떤 클래스에 넣을 생성자를 설계하는 과정이다.

 

1
2
3
4
5
6
class Date
{
public:
    Date(int month, int day, int year);
    ...
};
cs

 

문제점

 

 - 매개 변수의 전달 순서가 잘못될 여지가 있다.

1
Date d(303 , 2017); //월에 30이라는 값이 들어가짐
cs

 

 - 월과 일에 해당하는 숫자가 잘못된 숫자일 수도 있다.

1
Date d(340 , 2017); //3월 40일이라는 값이 들어가짐.
cs

 

 

개선 1-1

 

새로운 타입을 들여와 인터페이스를 강화하면 사용자의 실수를 막을 수 있다.

 

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
struct Day
{
    explicit Day(int d) : val(d) {    }
 
    int val;
};
 
struct Month
{
    explicit Month(int dm) : val(m) {    }
 
    int val;
};
 
struct Year
{
    explicit Year(int y) : val(y) {    }
 
    int val;
};
 
class Date
{
public:
    Date(const Month& m, const Day& d, const Year& y);
    ...
};
 
Date d(3302017);                    //타입이 틀려서 에러 발생.
 
Date d(Day(30), Month(3), Year(2017));  //타입이 틀려서 에러 발생.
 
Date d(Month(3), Day(30), Year(2017));  //정상 컴파일.
cs

 

 

개선 1-2

 

'월'이 가질 수 있는 유효한 값을 검사해야 하는데 enum을 사용해도 되지만 타입 안정성은 그리 믿음직하지 못하다.

타입 안정성이 신경 쓰인다면 유효한 Month의 집합을 미리 정의해 두어도 좋다.

 

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
class Month
{
public:
    static Month Jan() { return Month(1);  }
    static Month Feb() { return Month(2);  }
    static Month Mar() { return Month(2);  }
    ...
    static Month Dec() { return Month(12); }
 
private:
    explicit Month(int m);    //Month 값이 외부에서 생성되지 못하도록
                              //생성자를 private으로 선언.
};
 
Date d(Month::Mar(), Day(30), Year(2017));
cs

 

여기서 귀찮게 하나 하나 함수로 return하는 식으로 프로그램이 되어 있다.

아래와 같이 편하게 정적 객체로 사용할 수도 있는데.. 그러지 않은 이유는....

 

1
2
3
4
5
6
7
8
9
class Month
{
public:
    static int Jan = 1;
    static int Feb = 2;
    static int Mar = 3;
    ...
    static int Dec = 12;
};
cs

 

항목4에서 배운 '비지역 정적 객체들의 초기화 순서는 정해지지 않는다'는 특성 때문이다.

 

 

예시 2

 

예상되는 사용자 실수를 막는 다른 방법으로는 어떤 타입이 제약을 부여하여 그 타입을 통해 할 수 있는 일들을

묶어 버리는 방법이 있다.

제약 부여 방법으로 아주 흔히 쓰이는 예가 'const 붙이기'이다. 항목3에서 설명했듯이 operator*의 반환 타입을

const로 한정함으로서 사용자가 사용자 정의 타입에 대해 다음과 같은 실수를 저지르지 않도록 할 수 있었다.

 

1
if ( (a * b) = c ) // 비교하려고 했으나 a*b 에 c를 대입해 버린 코드.
cs

 

여기서 하고자 하는 얘기는 '별다른 이유가 없다면 사용자 정의 타입은 기본 제공 타입처럼 동작하게 만들어라'

라는 것이다. int 등의 타입 정도는 사용자들이 그 성질을 이미 다 알고 있기 때문에, 사용자를 위해 만드는 타입도

웬만하면 이들과 똑같이 동작하게 만들라는 것이다.

 

기본제공 타입과 쓸데없이 어긋나는 동작을 피하는 실질적인 이유는 일관성 있는 인터페이스를 제공하기 위해서이다.

제대로 쓰기에 괜찮은 인터페이스를 만들어주는 요인 중에 중요한 것은 일관성이다.

또한 불편한 인터페이스를 만들어주는 요인 중에 비일관성을 따라오는 것도 거의 없다.

 

사용자 쪽에서 뭔가를 외워야 제대로 쓸 수 있는 인터페이스는 잘 못 쓰기 쉽고 언제라도 잊어버릴 수 있다.

 

 

예시 3

 

항목 13에서 썼던 팩토리 함수를 한 번 보자.

 

1
Investment* createInvestment();
cs

 

이렇게 사용하면 자원 누출을 피하기 위해 createInvestment에서 얻어낸 포인터를 나중에라도 삭제해야 한다.

 

문제점 

 

 - 사용자가 포인터 삭제를 잊을 수 있다.

 - 똑같은 포인터에 대해 delete가 두 번 이상 적용될 수 있다.

 - 스마트 포인터에 createInvestment에서 얻은 포인터를 저장시키는 것을 잊을 수 있다.

 

개선

 

1
std::tr1::shared_ptr<Investment> createInvestment();
cs

 

애초에 팩토리 함수가 스마트 포인터를 반환하게 만들자.

이렇게 해 두면, 함수의 반환 값은 tr1::shared_ptr에 넣어둘 수 밖에 없을 뿐더러, 나중에 Investment 객체가

필요 없어졌을 때 객체를 삭제하는 것에 사용자가 신경쓸 필요가 없어진다.

 

tr1::shared_ptr을 반환하는 구조는 자원 해제에 관련된 상당수의 사용자 실수를 사전 봉쇄할 수 있고 생성 시점에

자원 해제 함수(삭제자)를 직접 엮을 수 있는 기능을 갖고 있기 때문에 설계자에게 좋다.

 

 

예시 4

 

createInvestment를 통해 얻은 Investment* 포인터를 직접 삭제하지 않게 하고 getRidOfInvestment라는 함수에

포인터를 넘겨 삭제하도록 하는 기능을 준비했다.

 

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
class Invest
{
public:
    ...
};
 
Invest* createInvest())
{
    return new Invest;
}
 
void getRidOfInvestment(Invest* obj)    //Invest 객체 삭제 전용 함수.
{
    delete obj;
}
 
void Fucn()
{
    Invest* pInvest = createInvest();
 
    ...        //pInvest 사용.
 
    getRidOfInvestment(pInvest);    //pInvest 객체 삭제.
}
cs

 

문제점

 

 - 사용자가 getRidOfInvestment를 사용하지 않고 직접 delete를 할 수 있다.

 - 실수로 delete도 하고 getRidOfInvestment도 호출하는 일이 발생할 수 있다.

 

개선

 

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
class Invest
{
public:
    ...
};
 
void getRidOfInvestment(Invest* obj) //삭제자.
{
    delete obj;
}
 
std::tr1::shared_ptr<Invest> createInvest()
{
    std::tr1::shared_ptr<Invest> ptr(new Invest, getRidOfInvestment); //초기화 포인터, 삭제자.
 
    return ptr;
}
 
 
int _tmain(int argc, _TCHAR* argv[])
{
    std::tr1::shared_ptr<Invest> pInvest = createInvest();
 
    return 0;
}
 
cs

 

createInvesetment 함수를 삭제자가 묶인 tr1::shared_ptr을 반환하도록 구현하자.

그러면 알아서 pInvest가 가리키는 객체가 0이 되면 삭제자를 호출하여 객체를 삭제할 것이다.

그런데 그냥 위에처럼 쓸거면 삭제자 자체를 만들 필요는 없다. 삭제자가 없어도 저 기능을 알아서 하기 때문이다.

 

삭제자를 별도로 사용하는 경우는 만약 일반 지역 변수의 주소를 스마트 포인터로 가리킬 때, 일반 지역 변수는 new로 생성된

것이 아니기 때문에 소멸 시 delete를 하면 안되므로 delete를 하지 않는 삭제자를 별도로 만들어주기 위해서 사용한다.

또는 배열을 스마트 포인터로 가리킬 경우에 delete[]를 사용해주기 위한 삭제자를 별도로 만들어줄 때 사용한다.

 

즉, 삭제자가 명시된 스마트 포인터는 스마트 포인터의 소멸자를 호출하지 않고 명시된 삭제자를 호출한다.

삭제자가 없는 일반 스마트 포인터는 소멸자가 호출되어 자신이 가리키던 객체의 Heap memory를 delete 시켜준다.

 

그리고 삭제자 함수를 사용할 때, 삭제자 함수는 스마트 포인터가 가리키는 포인터형의 파라미터를 반드시 가지고 있어야한다.

 

 

예시 5

 

tr1::shared_ptr는 '교차 DLL 문제'가 생기는 경우에도 이런 문제를 미연에 방지해주는 역할을 해준다.

'교차 DLL 문제'가 생기는 경우는 어떤 DLL에서 객체를 생성하고 이 객체를 다른 DLL에서 소멸시킬 때 이다.

즉, 객체가 생성(new)되고 삭제(delete)되는 것이 서로 다른 DLL에서 이뤄지면 안된다는 것이다.

 

그런데 tr1::shared_ptr을 사용하면 이 문제를 피할 수 있다. tr1::shared_ptr은 생성된 DLL과 동일한 DLL에서 delete를

사용하도록 삭제자가 만들어져 있기 때문이다.

 

1
2
3
4
5
6
7
8
9
10
class stock : Invest
{
    ...
};
 
std::tr1::shared_ptr<Invest> createInvest()
{
    return std::tr1::shared_ptr<Invest>(new stock);
}
 
cs

 

createInvest가 반환하는 shared_ptr은 다른 DLL들 사이에 이리저리 넘겨지더라도 교차 DLL 문제를 걱정하지 않아도 된다는

뜻이다.

 

4줄 요약

 

 - 좋은 인터페이스는 제대로 쓰기에 쉬우며 엉터로리로 쓰기에 어렵다. 인터페이스를 만들 때는 이 특성을 지닐 수 있도록

   고민해야 한다.

 

 - 인터페이스의 올바른 사용을 이끄는 방법으로는 인터페이스 사이의 일관성 잡아주기, 그리고 기본 제공 타입과의 동작

   호환성 유지하기이다.

 

 - 사용자의 실수를 방지하는 방법으로는 새로운 타입 만들기, 타입에 대한 연산을 제한하기, 객체의 값에 대해 제약 걸기,

   자원 관리 작업을 사용자 책임으로 놓지 않기가 있다.

 

 - tr1::shared_ptr은 사용자 정의 삭제자를 지원한다. 이 특징 때문에 tr1::shared_ptr은 교차 DLL 문제를 막아주며,

   뮤텍스 등을 자동으로 잠금 해제하는데 사용할 수 있다.

 

1
2
3
4
5
6
7
8
9
10
11
12
13
class Widget
{
   ...
};
 
int Calculate();
 
void processWidget(std::tr1::shared_ptr<Widget> pw, int Value);
 
void fucn()
{
    processWidget(new Widget, Calculate());
}
cs

 

위와 같이 스마트 포인터와 int 타입의 매개 변수를 가지는 processWidget 함수를 호출하려고 하면,

컴파일 에러가 발생한다.

 

포인터를 받는 tr1::shared_ptr의 생성자는 explicit으로 선언되어 있으므로, new Widget으로 생성된 포인터가

tr1::shared_ptr 타입의 객체로 바뀌는 암시적 변환이 이루어질 수 없기 때문이다.

 

그래서 하기와 같이 명시적 변환으로 함수를 호출해야 한다.

 

1
2
3
4
void fucn()
{
    processWidget(std::tr1::shared_ptr<Widget>(new Widget), Calculate());
}
cs

 

그런데 이 함수 호출 방식은 자원 관리 객체를 쓰고 있는데도 자원 누출이 될 가능성이 있다.

 

컴파일러는 processWidget 호출 코드를 만들기 전에 우선 이 함수의 매개변수로 넘겨지는 인자를 평가한다.

여기서 첫 번째 인자는 두 부분으로 나누어져 있는데 이 것이 문제의 원인이 될 수 있다.

 

첫 번째 인자가 나뉘는 두 부분.

 

 1. "new Widget" 표현식을 실행하는 부분.

 2. tr1::shared_ptr 생성자를 호출하는 부분.

 

그러므로 processWidget 함수 호출이 이루어지기 전에 컴파일러는 세 가지 연산 코드를 만들어야 한다.

 

 1. Calculate 함수 호출.

 2. "new Widget"을 실행.

 3. tr1::shared_ptr 생성자 호출.

 

그런데 문제는 각 각의 연산이 실행되는 순서가 컴파일러 제작사마다 다르다는 점이다.

자바 및 C#은 매개변수의 평가 순서가 특정하게 고정되어 있는 반면에 C++의 평가 순서는 높은 자유도를 가진다.

그래서 어떤 컴파일러의 경우에는 아래와 같은 순서로 진행될 수 있다.

 

 1. "new Widget"을 실행.

 2. Calculate 함수 호출.

 3. tr1::shared_ptr 생성자 호출.

 

만약 이러한 호출 과정을 진행하는 도중에, Calculate 함수에서 예외가 발생한다면 "new Widget'으로 만들어졌던 포인터가

스마트 포인터에 저장이 되지 않는 문제가 발생해 버린다.

즉, processWidget 호출 중에 자원이 누출될 가능성이 있는 이유는, 자원이 생성되는 시점("new Widget")과 새성한 자원이

자원 관리 객체로 넘어가는 시점 사이에 예외가 끼어들 수 있기 때문이다.

 

해결 방법

 

 - Widget을 생성해서 스마트 포인터에 저장하는 코드를 별도의 문장 하나로 만들고, 그 스마트 포인터를 ProcesWidget에

   넘기는 방법이다.

 

1
2
3
4
5
6
7
8
void fucn()
{
    std::tr1::shared_ptr<Widget> pw(new Widget); //new Widget으로 생성된 포인터를
                                                 //스마트 포인터에 저장하는
                                                 //별도의 문장.
 
    processWidget(pw, Calculate());
}
cs

 

1줄 요약

 

 - new로 생성한 객체를 스마트 포인터로 넣는 코드는 별도의 한 문장으로 만들자.
   이것이 안되어 있으면, 예외가 발생될 때 디버깅하기 힘든 자원 누출이 초래될 수 있다.

+ Recent posts