레이블이 lnline인 게시물을 표시합니다. 모든 게시물 표시
레이블이 lnline인 게시물을 표시합니다. 모든 게시물 표시

2016년 3월 23일 수요일

[Effective C++] 항목 21 : 함수에서 객체를 반환해야 할 경우에 참조자를 반환하려고 들지 말자.

유리수를 나타내는 클래스
class Rational {
public:
           Rational(int numerator = 0, int denominator = 1); // 생성자가 explicit로 선언되지 않은 이유는(24)
           ...
private:
           int n, d; // 분자 및 분모
           friend const Rational operator* (const Rational& lhs, const Rational& rhs); // 반환타입이const인 이유는 (3)
};

이 클래스의 operator*는 곱셈 결과를 값으로 반환하도록 되어 있다.

값이 아닌 참조자를 반환할 수 있으면 비용 부담은 없을 것입니다그러나 참조자는 그냥 이름입니다존재하는 객체에 붙는 다른 이름입니다.
operator*가 참조자를 반환하도록 만들어졌다면이 함수가 반환하는 참조자는 반드시 이미 존재하는 Rational 객체의 참조자여야 합니다.

그럼 반환될 객체는 어디에 있을까요?
Rational a(1, 2); // a = 1/2
Rational b(3, 5); // b = 3/5

Rational c = a * b; // c 3/10 이어야 합니다.

객체에 대한 참조자를 operator*에서 반환할 수 있으려면그 객체를 직접 생성해야 한다는 것입니다.

함수 수준에서 새로운 객체를 만드는 방법은 딱 2가지 뿐입니다하나는 스택에 만드는 것이고,또 하나는 힙에 만드는 것입니다.

우선 전자의 방법은스택에 객체를 만들려면 지역 변수를 정의하면 됩니다.

const Rational$ operator*(const Rational& lhs, const Rational& rhs) // 어이없는 코드
{
           Rational result(lhs.n * rhs.n, lhs.d * rhs.d);
           return result;
}

위 예제는 피하자.생성자가 불리는 게 싫어서 시작한 일인데결국 result가 다른 객체처럼 생성되어야 하잖아요.더 심각한 문제는이 연산자 함수는 result에 대한 참조자를 반환하는데, result는 지역 객체입니다함수가 끝날 때 덩달아 소멸되는 객체죠.
그러니까 이 operator*는 현재 온전한 Rational 객체에 대한 참조자를 반환하지 않습니다.
이 함수를 호출한 쪽은 그 즉시 미정의 동작에 빠지게 됩니다.

후자의 방법함수가 반환할 객체를 힙에 생성했다가그 녀석의 참조자를 반환하는 것입니다.

const Rational$ operator*(const Rational& lhs, const Rational& rhs) // 또 일났네요!
{
           Rational *result = new Rational(lhs.n * rhs.n, lhs.d * rhs.d);
           return *result;
}

여전히 생성자가 한번 호출되기는 매한가지입니다.
new
로 할당한 메모리를 초기화할 때 생성자가 호출되니 말입니다
.이걸 말고도, new로 만든 객체를 누가 delete해줄까요?
이 함수를 호출하는 쪽에서는 메모리 누출을 막기에는 한계가 있습니다.

Rational w, x, y, z;
w = x * y * z // operator* (operator* (x, y), z) 와 같습니다.

여기서는 한 문장 안에서 operator* 호출이 2번 일어나고 있기 때문에, new에 짝을 맞추어 delete를 호출하는 작업도 2번이 필요합니다그런데 operator*의 사용자 쪽에서는 이렇게 할 수 있는 합당한 방법이 없습니다.
operator*
로부터 반환되는 참조자 뒤에 숨겨진 포인터에 대해서는 사용자가 어떻게 접근할 방법이 없기 때문입니다.

 2개의 예는한 가지 문제를 똑같이 가지고 있었습니다.스택 기반으로 하든 힙 기반으로 하든 operator*에서 반환되는 결과는반드시 생성자를 꼭 한 번 호출했을 거예요그런데필요 없는 생성자 호출을 피해 보자는 것이 아마 처음 세운 목표였을 걸요?

const Rational$ operator*(const Rational& lhs, const Rational& rhs) // 경고!
{
           static Rational result; // 반환할 참조자가 가리킬 정적 객체.
           result = ...;
           return result;
}

위 코드는 스레드 안정성 문제가 있습니다 
또한 아래와 같은 문제가 있습니다.

bool operator==(const Rational& lhs, const Rational& rhs);
Rational a, b, c, d;
...
if ((a*b) == (c*d)) {
           두 유리수 쌍의 곱이 서로 같으면 적절한 처리를 수행
} else {
           다르면 적절한 처리를 수행
}

((a*b) == (c*d)) 표현식이 항상 true 값을 냅니다.
a,b,c,d
에 어떤 값이 들어가도 마찬가지입니다.

위의 표현식을 아래처럼 바꾸면

if (operator==(operator*(a,b), operator*(c,d)))

operator== 가 호출될 때, operator*가 활성화되어 있을 것이고각각의 호출을 통해 operator*안에 정의된 정적 Rational 객체의 참조자가 반환될 것입니다.
operator==
가 비교하는 피연산자는 operator* 안의 정적 Rational 객체의 값 입니다
.이 둘이 같지 않은 것이 더 이상합니다.

새로운 개체를 반환해야 하는 함수를 작성하는 방법에는 정도(正道)가 있습니다바로 새로운 객체를 반환하게 만드는 것이죠
그러니까 Rational의 operator*는 아래처럼 혹은 비슷하게 작성해야 합니다.

inline const Rational operator* (const Rational& lhs, const Rational& rhs)
{
           return Rational(lhs.n * rhs.n, lhs.d * rhs.d);
}

이 코드에도 반환 값을 생성하고 소멸시키는 비용이 들어 있습니다그러나 끝까지 따져 보면 여기에 들어가는 비용은 올바른 동작에 지불되는 작은 비용입니다.
C++에서도 다 컴파일러 구현자들이 가시적인 동작 변경을 가하지 않고도 기존 코드의 수행 성능을 높이는 최적화를 적용할 수 있도록 배려해 두었습니다그 결과몇몇 조건하에서는 이 최적화 메커니즘에 의해 operator*의 반환 값에 대한 생성과 소멸 동작이 안전하게 제거될 수 있습니다.(반환 값 최적화(return value optimization) RVO)

l  지역 스택 객체에 대한 포인터나 참조자를 반환하는일혹은 힙에 할당된 객체에 대한 참조자를 반환하는 일또는 지역 정적 객체에 대한 포인터나 참조자를 반환하는 일은 그런 객체가 2개 이상 필요해질 가능성이 있다면 절대로 하지 마세요. (4)를 보면 지역 정적 객체에 대한 참조자를 반환하도록 설계된 올바른 코드 예제를 찾을 수 있습니다.

2016년 3월 17일 목요일

[Effective C++] 항목 2. #define을 쓰려거든 const, enum, inline을 떠올리자

아래와 같은 define 문이 있을 때,
#define ASPECT_RATIO 1.653
ASPECT_RATIO라는 이름은 심볼은 컴파일러에겐 전혀 보이지 않는다(컴파일러의 심볼 테이블에 들어가지 않음). 선행 처리자가 숫자 상수로 바꾸어 버리기 때문이다.
이 때문에 생길 수 있는 문제는 다음과 같다.
  • 컴파일 에러가 발생하면 ASPECT_RATIO라는 심볼보다는 1.653이라는 상수를 마주하게 될 것이므로, 버그를 찾기가 어려워 진다. (더군다나 ASPECT_RATIO가 정의된 파일이 프로젝트 내에 있지 않을 경우, 찾기가 더욱 곤란해 질 수 있음)
  • 심볼릭 디버거에서도 ASPECT_RATIO라는 심볼 대신에 숫자를 보여주므로 디버깅이 어려워 질 수 있다.
이 문제를 해결할 수 있는 방법은 매크로 대신 상수를 쓰는 것이다.
const double AspectRatio = 1.653; // 대문자로만 표기하는 이름은 보통 매크로에서 쓰는 것이라서, 이름 표기도 바꿔줌
AspectRatio컴파일러의 심볼 테이블에 들어가게 된다. 
또한 추가로 얻을 수 있는 이점은 컴파일된 코드의 크기가 작아질 수 있다는 것인데, #define을 쓸 경우 사용된 개수만큼 해당 숫자의 사본이 생기게 되는데, 상수의 경우는 사본이 딱 한개만 생기기 때문이다(몇몇 CPU 아키텍쳐에서는 작은 정수 값에 대해서 Instruction Code 내부에 Immediate 타입의 값을 직접 저장할 수 있으므로, 해당이 되지 않을 수 있음).
#define 을 상수로 교체하려는 경우, 두가지 경우만 조심하자.
  1. 상수 포인터를 정의하는 경우: 보통 헤더 파일에 넣는 것이 관례이므로, 포인터는 꼭 const로 선언해 주어야 하고, 포인터가 가리키는 대상까지 const로 선언해 주어야 한다.
  2. const char* const authorName = "Scott Meyers"; // const의 의미와 사용법에 대한 자세한 사항은 항목 3 참조
    문자열 상수에는 char*같은 구닥다리 문자열 보다는 string 객체가 더 사용하기 편하다.
    const std::string authorName("Scott Meyers");
  3. 클래스 상수를 정의하는 경우: 어떤 상수의 유효범위를 클래스로 한정하고자 할 때, 그 상수의 사본 개수가 한개를 넘지 못하게 하고 싶다면 정적(static) 멤버로 만들어야 한다.
  4. class GamePlayer {
    private:
        static const int NumTurns = 5; // 상수 선언(declaration)
        int scores[NumTurns];
        // ...
    };
C++ 에서는 대부분의 것들에서 정의가 마련되어 있어야 하지만, 정적 멤버로 만들어지는 정수류(각종 정수 타입, charbool 등) 타입의 클래스 내부 상수는 예외이다.
이들에 대해 주소를 취하지 않는 한, 정의 없이 선언만 해도 아무 문제가 없다.
단, 클래스 상수의 주소를 구해야 한다면 얘기가 달라진다.
const int GamePlayer::NumTurns;
이 클래스 상수의 정의는 구현 파일에 두어야 한다. 또한 클래스 상수의 초기값은 해당 상수가 선언된 시점(헤더 파일)에 바로 주어지기 때문에 정의(구현 파일)에는 초기 값을 주지 않는다.
상수의 주소를 구한다거나, 상수의 참조자를 취하는 일을 막으려면 enum을 쓰면 된다.
class GamePlayer {
private:
    enum { NumTurns = 5 };
    int scores[NumTurns];
    // ...
};
const의 주소를 구하는 것은 합당하지만, enum의 주소를 구하는 것은 안되기 때문이다. 
enum은 어떠한 형태의 쓸데없는 메모리 할당도 절대 저지르지 않는다.


매크로 함수 대체 하기 
#define을 잘못 사용하는 경우는 종종 매크로 함수에서 볼 수 있다.
// 매크로 함수의 인자는 항상 괄호로 싸서, 표현식이 변형되는 것을 막아주자.
#define CALL_WITH_MAX(a, b) func((a) > (b) ? (a) : (b))
int a = 5, b = 0;
CALL_WITH_MAX(++a, b); // a가 두 번 증가
CALL_WITH_MAX(++a, b+10); // a가 한 번 증가
이처럼 표현식의 결과에 따라 인자가 평가되는 횟수가 달라진다.
C++ 에서는 기존 매크로의 효율을 그대로 유지하면서 정규 함수의 모든 동작방식 및 타입 안전성까지 완벽하게 취할 수 있는 방법이 있다. 
바로
인라인 템플릿 함수(항목 30 참조)를 만드는 것이다.
template<typename T>
inline void callWithMax(const T& a, const T& b) // T가 정확히 어떤 타입인지 모르기 때문에, 상수 객체에 대한 참조자를 씀. (항목 20 참조)
{
    f(a > b ? a : b);
}
함수 본문에 지저분하게 괄호를 넣을 필요도 없고, 인자를 여러 번 평가하지도 않는다.
뿐만 아니라 진짜 함수이기 때문에, 유호범위 및 접근 규칙을 그대로 따라간다.
임의의 클래스 안에서만 쓸 수 있는 인라인 함수가 가능하다는 얘기다.
  • 단순한 상수를 쓸 때는, #define 보다 const 객체 혹은 enum 을 우선 생각하자.
  • 함수처럼 쓰이는 매크로를 만들려면, #define 매크로보다 인라인 함수를 우선 생각하자.

[Effective C++] 항목 30 : 인라인 함수는 미주알고주알 따져서 이해해 두자.

인라인 함수를 사용하면 컴파일러가 함수 본문에 대해 문맥별 최적화를 걸기가 용이해집니다. 인라인 함수의 아이디어는  함수 호출문을 그 함수의 본문으로 바꿔치기하자는 것  남발했다가는 코드의 크기가 커질 게 뻔하다. 인라인 함수로 부풀려진 ...