C++이야기 스물여섯번째: constness를 활용한 멤버 함수 overloading

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


C++로 작성된 클래스 API에서 멤버 함수 끝에 const가 붙은 걸 보신적 있으신가요? 상수객체에 대고 const가 붙어 있지 않은 멤버 함수를 호출할 수 없는 것도 다 아시리라 생각합니다.

/// @file   const.cpp
#include <iostream>

using namespace std;

class Foo
{
public:
    void modify()
    {
        cout << __PRETTY_FUNCTION__ << " called" << endl;
    }

    void dontModify() const
    {
        cout << __PRETTY_FUNCTION__ << " called" << endl;
    }
};

int
main()
{
    const Foo foo;

    foo.modify();
}

이걸 g++로 컴파일하면 다음과 같은 에러가 발생합니다.

error: passing 'const Foo' as 'this' argument of 'void Foo::modify()' discards qualifiers

해석하자면 const Foo를 Foo::modify()의 인자로 넣게 되면 constness qualifier가 없어져 버린다는 뜻이죠. Foo::modify()는 변경가능한 this를 필요로 하는데(함수 Signature 마지막에 const가 붙어 있질 않습니다), const Foo를 변경가능한 this로 바꿔서 넣어주면 안된다는 얘기입니다. modify()를 호출하지 않고, dontModify()를 호출하면 컴파일도 잘되고 실행도 잘됩니다.

int
main()
{
    const Foo foo;

    foo.dontModify();
}

$ ./const
void Foo::dontModify() const called

이런 const 멤버 함수 특성을 이용하면 클래스 API의 사용자가 실수로 상수 객체를 변경하는 일을 막을 수 있게 됩니다. 그래서 C++ 좀 쓸 줄 안다하는 사람은 const를 애용해서 사용합니다. 저도 const를 애용하고 있구요. (흠... 이건 뭥미? 지 자랑? 쿨럭 쿨럭)

C++를 좀 쓸 줄 안다고 자신하던 여러분에게 새로운 도전이 왔습니다. const 객체인 경우와 그렇지 않은 객체에 대해 동일한 기능을 하는 멤버 함수를 제공해야 하고, 그 멤버 함수가 워낙 자주 사용되기 때문에 성능도 좋게 해야할 필요가 있다는 것입니다. 예를 들어, Foo 클래스의 멤버 함수로 toString()이 있는데 이게 그런 case라고 생각해 보겠습니다. 여러분이라면 어떻게 이 도전에 응전을 하시겠습니까?

이렇게 하면 어떨까요?

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

    string toStringConst() const
    {}
};

즉, 비상수 객체를 위한 toString() 과 상수객체를 위한 toStringConst()를 따로 두는 것이지요. 이것도 괜찮은 방법인 것 같습니다. 그런데, 이렇게 할 경우 사용자가 자신이 상수 객체를 다룰 때와 비상수 객체를 다룰 때 어떤 멤버 함수를 쓸 수 있는지를 따로 기억하고 있어야 하는 부담이 있습니다. 물론, Foo야 별 문제가 없지만 멤버 함수가 많은 클래스라면 어떻게 될까요? 항상 시스템이 간단할 때는 별로 문제가 되지 않지요. 복잡해질때 문제가 되니까요.

바로 이럴 때 활용할 수 있는 방법이 constness를 활용한 멤버 함수 overloading입니다. 다음과 같이 toString()을 정의하면 원하는 목적을 달성할 수 있을 것 같습니다.

class Foo
{
public:
    string& toString()
    {
        cout << __PRETTY_FUNCTION__ << " called" << endl;
    }

    string toString() const
    {
        cout << __PRETTY_FUNCTION__ << " called" << endl;
    }
};

int
main()
{
    Foo foo1;
    const Foo foo2;
    Foo& rfoo1 = foo1;
    const Foo& rfoo2 = foo2;

    foo1.toString();
    rfoo1.toString();
    foo2.toString();
    rfoo2.toString();
}

이걸 컴파일해서 실행해 보면 다음과 같은 결과가 출력됩니다.

$ ./const
std::string& Foo::toString() called
std::string& Foo::toString() called
std::string Foo::toString() const called
std::string Foo::toString() const called

즉 객체가 비상수 객체냐 상수 객체냐에 따라 적절한 멤버 함수가 호출되는 것이지요. 게다가 const 가 붙지 않은 경우에는 string&를 리턴해서 리턴값이 복사되지 않도록 하면서 성능도 최적화했고, 클래스 API 사용자도 toString()이라는 멤버 함수만 기억하면 되니 한결 수월해지겠지요. 이 방법은 함수의 Signature에 const가 포함되는 사실을 이용한 것입니다.

여러분이 설계하는 클래스 API에 const를 활용한 overload 기법을 적용한다면 제가 장담하건데 그걸 보는 사람들이 "C++ 좀 많이 쓸 줄 아네"라고 할 것이니 잘 활용해 보시기 바랍니다. ^^

다음글에서는 const 멤버 함수와 밀접한 관련이 있는 mutable 키워드에 대해 알아보도록 하겠습니다.