C++ 이야기 열여덟번째: 임시 객체는 임시 객체일 뿐 A/S

Posted at 2008.03.08 18:00 // in S/W개발/C++ 이야기 // by 김윤수


[이글의 최신 Update 문서는 항상 여기에서 확인할 수 있습니다]

2008/02/24 - [S/W개발/C++ 이야기] - C++ 이야기 열일곱번째: 임시 객체는 임시 객체일 뿐

위 글을 썼더니 어떤 분이 아래와 같은 얘기를 하시더군요.

말씀하신 operator const char*()는 문제가 있는 것 같습니다. 일반적으로 포인터를 리턴하는 함수의 경우 리턴되는 포인터는 함수 내부에서 new 되거나, 전역 static 변수인 경우에서만 특별히 허용됩니다. 그래야 말씀하신 것과 같은 dangling reference 문제가 없지요. 이것은 (const char*)처럼 explicit casting을 한다고 해서 해결될 문제가 아닌 것 같습니다. 그래서는 너무 error-prone 하지요
제가 글을 작성한 의도를 약간 오해하신 것 같아 부연 설명하려고 합니다.

우선 "일반적으로 포인터를 리턴하는 함수의 경우 리턴되는 포인터는 함수 내부에서 new 되거나, 전역 static 변수인 경우에서만 특별히 허용됩니다. 그래야 말씀하신 것과 같은 dangling reference 문제가 없지요"라고 말씀하신 것은 상당히 많은 오해의 소지가 있습니다. 왜 그런지 설명하기 위해서는 지난 번에 제가 썼던 글을 자세히 뜯어 봐야할 것 같습니다.

그럼 다음과 같은 코드는 어떻게 될까요 ? operator const char*() 의 구현이 따로 복사본을 리턴하는 것이 아니라 내부 char 배열에 대한 pointer 만 넘겨 주도록 구현되어 있다고 가정해 보시기 바랍니다(보통은 이런식으로 optimize 를 해서 구현하기 마련이죠).

class String {
  // ...
public:
  friend String operator+(const String&, const String&);
  // ...
  operator const char*();
    // conversion operator to C-style string
};

void f(String s1, String s2)
{
  const char* p = s1+s2;    // 바로 이 부분이 문제!!
  printf("%s", p);
  // ...
}
더 정확히 저의 의도를 표현하기 위해 다음과 같이 코드를 수정했습니다.

class String {
private:
  char* rep_;
  unsigned len_;

  // ...

public:
  friend String operator+(const String&, const String&);
  // ...
  // conversion operator to C-style string
  operator const char*()
  {
    return rep_;
  }

};

String operator+(const String& lstr, const String& rstr)
{
  String str(lstr.len + rstr.len + 1); // String(unsigned) 가 있다고 가정
  strcpy(str.rep_, lstr.rep_);
  strcat(str.rep_, rstr.rep_);
  str.len_ = lstr.len_ + rstr.len_;

  return str;
}


void f(String s1, String s2)
{
  const char* p = s1+s2;    // 바로 이 부분이 문제!!
  printf("%s", p);
  // ...
}


operator const char*() 를 위와 같이 구현하지 않고 내부적으로 새로  할당된 char 배열이나 static 으로 선언된 char 배열을 리턴한다면 dangling reference 문제가 없어질 것이다라는 취지로 말씀하셨을 것입니다. 말씀하신 대로 dangling reference 문제는 없어질 것입니다. 그렇다면 String 객체 내부 char 배열을 리턴하지 않고, 다음과 같이 새로 할당하여 리턴한다면 임시 객체로 인한 문제가 사라질까요 ?

operator const char* ()
{
  char * nrep_ = new char[len_ + 1];
  strcpy(nrep_, rep_);

  return nrep_;
}


그렇지 않습니다. dangling reference 문제는 사라지지만 memory leak 문제가 발생할 가능성이 높습니다. (물론 잘 눈에 띄진 않지만 copy 로 인한 performance 문제도 있지요) 다음과 같은 코드는 겉보기에는 멀쩡해 보이지만 memory leak 이 발생하게 될 것입니다.

void f(String s1, String s2)
{
  printf("%s", (const char*)(s1+s2));
  // ...
}


(const char*) 라는 casting을 만나면 컴파일러는 operator const char *()를 호출할 것이고, 그 안에서 동적 할당되어 리턴된 char 배열을 가리키고 있는 변수가 없으니 memory leak 이 발생하는 것이지요.

그렇다고, const char* p = s1+s2 와 같이 코드를 작성한 사람이 알아서 꼬박 꼬박 delete[] p 를 호출해야 한다는 건 너무 까다로운 요구가 될 것입니다. 다음과 같이 코드를 작성해야 한다는 걸 프로그래머에게 강제한다는 건 글쎄요... 결코 좋은 string 클래스라고 보기는 그렇네요.

void f(String s1, String s2)
{
  const char* p = s1+s2;
  printf("%s", p);
  delete[] p;
  // ...
}


operator const char* () 는 리턴하는 타입이 const char* 이라는 것에 주목해 보시기 바랍니다. 이것은 가리키는 대상인 char 배열의 내용을 바꿀 수 없다는 것을 뜻합니다. 내용을 바꿀 수 없다는 것에는 메모리를 릴리즈할 수 없다는 것도 포함됩니다(이 정도까지 체크해서 에러 메시지를 출력해주는 컴파일러는 별로 없는 것 같습니다). const char* 를 리턴했다는 것 자체가 리턴값을 받아쓰는 쪽에서는 포인터가 가리키는 대상을 절대 수정해서는 안된다는 의미를 담고 있기 때문입니다. 그런데도 delete[] 로 메모리를 릴리즈하도록 프로그래머에게 요구하는 건 잘못된 API 설계라고 봐야할 것입니다.

그렇다면, 다음과 같이 내부 static char 배열에 대한 포인터를 리턴하도록 구현한다면 문제가 없어질까요 ?

operator const char* ()
{
  static char nrep_[MAX_STR_LEN];
  strcpy(nrep_, rep_);

  return nrep_;
}


그렇지 않습니다. dangling reference 문제나 memory leak 문제는 발생하지 않지만 새로운 두 가지 문제가 발생하게 됩니다(이 구현도 copy 로 인한 performance 문제가 있습니다)

- MAX_STR_LEN 를 어느 정도로 정해야 할지가 까다로워집니다.
- 위의 operator const char* 는 thread-safe하지도 않고 reentrant 하지도 않은 코드가 되므로 multi-threaded 환경에서는 쓰기가 어려워집니다.

위와 같은 여러 문제점들을 종합적으로 고려했을 때, String 객체 내부의 char 배열인 rep_ 를 리턴하는 것이 그리 나쁜 선택이 아님을 알 수 있습니다. 특히 성능 측면에서는 좋은 선택이라 할 수 있습니다.

마지막으로 생각할 수 있는 다른 대안은 operator const char* () 를 제공하지 않는 것입니다. 이렇게 할 경우 C++의 기원상 C 코드와의 혼합 프로그래밍을 피할 수 없음에도 C와의 interoperability 를 막게되므로 String 의 사용성을 상당히 떨어뜨리게 되고, 결국 String class는 잘 사용되지 않게 될 것입니다. 굳이 operator const char* 형태는 아니더라도 C++ standard library의 string template class 처럼 c_str() 이라는 메소드와 같은 형태로라도 C와의 혼합 프로그래밍을 지원해 줘야할 것이고, c_str()은 다시 이 글에서 설명한 operator const char* () 와 동일한 문제점을 갖게 될 것입니다.

그리고, 마지막으로 한 가지 더 언급하자면 C++ standard library 중의 하나인 string template class에 정의된 c_str() 메소드의 구현 방식도 대부분 implementation에서 여기에서 설명한 operator const char * ()의 구현방식과 비슷하게 string class 내부에 할당되어 있는 char 배열을 리턴하는 식으로 구현되어 있습니다. 이 글에서 언급한 여러가지 문제들을 고려한 결과 가장 문제가 적은 구현 방식을 취한 거라 생각합니다. 어차피 모든 경우에 완벽한 코드란 있기 힘드니까요. ^^

제 글이 유익하셨다면 오른쪽 버튼을 눌러 제 블로그를 구독하세요. ->
블로그를 구독하는 방법을 잘 모르시는 분은 2. RSS 활용을 클릭하세요.
RSS에 대해 잘 모르시는 분은 1. RSS란 무엇인가를 클릭하세요.

다음글 예고편: C++ 이야기 열아홉번째: 임시 객체가 충분히 임시스럽기 위하여
다음글도 기대해 주세요~~~ :)

신고
  1. 고어핀드

    2008.03.13 08:53 신고 [수정/삭제] [답글]

    그래서인지 c_str() 메소드를 쓰는 데 있어서는 "항상 리턴값을 그대로 쓰지 말고 strcpy로 복사해 써라." 고 하더군요. string 클래스를 배울 때부터 그렇게 배워서 지금까지 그렇게 버릇들여서 쓰고 있습니다.

댓글을 남겨주세요.