C++이야기 스물일곱번째: C++에 돌연변이가 있었나?

Posted at 2008. 11. 16. 16:30 // in S/W개발/C++ 이야기 // by 김윤수


2008/11/15 - [S/W개발/C++ 이야기] - C++이야기 스물여섯번째: constness를 활용한 멤버 함수 overloading

이전 글 말미에 const 멤버 함수와 밀접한 관련이 있는 mutable에 소개해 드린다고 했는데... 왜 제목을 "C++에 돌연변이가 있었나?"라고 지었을까요?

X-Men 1,2 보신 분들이나 요즘 유행하는 미드 중 Heroes를 즐겨 보시는 분들은 잘 아시겠지만 돌연 변이를 영어로 mutant라고 합니다. 왠지 mutable과 mutant가 서로 형제지간일 것 같지 않으세요? 그렇습니다. mutable의 사전적 의미는 변할 수 있는이라는 뜻으로 constant와는 반대의 뜻을 가지고 있습니다.

이 대목에서 mutable과 const의 복잡 미묘한 관계를 읽을 수 있는데요... 겉으로 보기에 서로 티격태격 싸우는 사이일 것 같지만 실제로는 서로 힘을 합쳐 멋진 코드를 만들어 낸답니다.

지난 번에 이어 class Foo를 예제로 들어 얘기 보따리를 풀어 보도록 하겠습니다.

지난 글에서 class Foo 에 다음과 같이 toString()과 toString() const를 정의했었습니다.
(실제 코드에서는 toString()과 같은 멤버 함수가 string&를 리턴할 리는 없을 것입니다. 그냥 예제로 든 것이니 toString()과 같은 멤버 함수를 이런식으로 구현하는 일은 피하시기 바랍니다)

class Foo
{
public:
    string& toString();
    string toString() const;
};

이렇게 정의해 놓고 한참을 잘 사용하고 있었는데, 갈수록 프로그램이 방대해지다 보니 어딘가 모르게 프로그램이 느려진다는 고객의 claim이 있어 성능 프로파일러를 돌려 봤더니 string toString() const이 무척 자주 호출된다는 걸 발견하게 됐습니다. 그래서 string toString() const의 성능을 개선하기로 마음을 먹었습니다. 한참 코드를 분석해 본 결과, toString()으로 리턴하는 값은 별로 자주 바뀌지 않기 때문에 string toString() const가 호출될 때마다 매번 값을 계산해서 리턴할 필요가 없다고 결론을 내리게 됐습니다. 그래서 이제는 내부적으로 계산된 값을 유지하고 그 값을 바로 리턴하는 식으로 구현을 바꾸려고 합니다. 물론, 내부적으로 유지되는 미리 계산된 값은 계산이 필요한 경우에 바로 update 하도록 하구요. 그래서 먼저, m_str이라는 멤버 변수와 m_needCompute 멤버 변수를 추가하기로 했습니다.

class Foo
{
public:
    string& toString();
    string toString() const;

private:
    string m_str;
    bool m_needCompute;
};

눈누난나하면서 다음과 같이 string Foo::toString() const를 구현하고 컴파일합니다.

string
Foo::toString() const
{
    // 새로 계산이 필요한 경우에만 새로 계산한 후... 계산된 값을 m_str에 assign 합니다.
    if (m_needCompute)
        m_str = computeString();

    return m_str;
}

아니 그런데 이게 왠 일입니까? 컴파일해 봤더니 바로 에러가 뜨는 것 아닙니까?

error: passing 'const std::string' as 'this' argument of 'std::basic_string<_CharT, _Traits, _Alloc>& std::basic_string<_CharT, _Traits, _Alloc>::operator=(const _CharT*) [with _CharT = char, _Traits = std::char_traits<char>, _Alloc = std::allocator<char>]' discards qualifiers

음... 이눔의 컴파일러 또 난리네... 투덜투덜!

자세히 들여다 봤더니 m_str = computeString() 이 부분이 말썽이네요. const 멤버 함수에 넘겨지는 this 는 상수 객체이고 당연히 멤버들도 모두 상수 객체인데, m_str 값을 변경하고 있다고 컴파일러가 투덜거렸던 것입니다. 이유 있는 반항이네요.

이럴 때 잘쓰는 비법 컴파일러 속이기 신공을 발휘합니다. 니가 뛰면 나는 난다. 크하하하 쾌재를 부르며 코드를 다음과 같이 고칩니다.

string
Foo::toString() const
{
    // 새로 계산이 필요한 경우에만 새로 계산한 후... 계산된 값을 m_str에 assign 합니다.
    if (m_needCompute)
        const_cast<Foo*>(this)->m_str = computeString();

    return m_str;
}

에러없이 잘 컴파일 되는군요. 그럼 그렇지 지가 무슨 용가리 똥배라고 나의 컴파일러 속이기 신공을 피할 수 있겠어. 이제 mentor 선배에게 코드 리뷰 받아야지. 선배에게 새로 작성한 코드를 보여 줬더니 오른쪽 눈썹이 파르르 떨리는 게 표정이 좋질 않습니다.

선배: "string Foo::toString() const 구현에서 꼭 const_cast를 써야 하나요? XXX씨는 너무 컴파일러 속이기를 잘 쓰는 것 같애~ 컴파일러 속이기 신공은 정말 정말 어쩔 수 없을 때만 쓰는 거예요. 컴파일러의 type system을 우회하는 것이기 때문에 언제 버그가 끼어들지 모르는 방식이잖아요"

XXX: (에이~ 또 괜한 생트집 잡는다고 생각하며)"거기에서 m_str 멤버 변수를 수정해야 하는데 어떡해요. 그렇다고 const를 뗄 수는 없구요."

선배: "그럼 m_str 앞에 mutable을 붙여 보세요. 그럴 때 쓰라고 있는 게 mutable인데?"

XXX: (O.O 엥? mutable이 뭐지? 이 선배가 요즘 Heroes를 보더니 mutant랑 뭐가 헤깔리나?)"mutable이요? mutable이 뭔데요? 그런게 있어요?"

선배: "mutable은 상수 멤버 함수에서도 수정이 필요한 멤버 변수에 붙이는 거예요. 상수 멤버 함수 안에서 그 멤버 변수를 수정하더라도 컴파일에러가 나지 않죠"

XXX: (설마 그럴리가? 나도 C++이라면 좀 한다하는 놈인데 그런 듣보잡이 있을리가)"정말 그런게 있어요?"

선배: "그런 게 있죠. 언제적부터 있던 건데... 얼른 수정하세요. 그것만 수정하면 다른 곳은 다 잘 작성했네요"

약간 자존심은 상했지만 코드를 다음과 같이 수정한 후 컴파일해 보았습니다.

class Foo
{
public:
    string& toString();
    string toString() const;

private:
    mutable string m_str;
    bool m_needCompute;
};

string
Foo::toString() const
{
    // 새로 계산이 필요한 경우에만 새로 계산한 후... 계산된 값을 m_str에 assign 합니다.
    if (m_needCompute)
        m_str = computeString();

    return m_str;
}

오~~~ 아무런 에러 없이 컴파일이 되네~~~~? 우왕ㅋ굳ㅋ 선배가 괜히 선배가 아니었네. 이야~ 덕분에 돌연변이 신공도 하나 배웠네.

어때요? 재미 있으셨나요? 약간은 이야기 식으로 설명해 보았는데 잘 이해가 되셨는지 모르겠습니다. 요약하자면

"const 멤버 함수 안에서도 수정이 필요한 멤버 변수가 있다면 mutable을 사용하자"

입니다. mutable과  const 의 하모니로 만들어낸 멋진 코드였습니다.