3. Implementation/C / C++

가능한 const를 이용한다.

SSKK 2008. 8. 24. 22:50
포인터를 위해선 포인터 그 자체, 포인터가 가리키는 데이터, 또는 이 모두를 const로 지정할 수 있다.

문장

포인터 (p)

데이터 (“Hello”)

char * p = “Hello”;

비상수

비상수

const char * p = “Hello”;

비상수

상수

char * const p = “Hello”;

상수

비상수

const char * const p = “Hello”;

상수

상수


이 문법은 보이는 것만큼 어렵진 않다. 포인터 선언의 * 를 기준으로 왼쪽에 const가 나타나면 포인터로 가리켜 지는 것(데이터)이 상수이고, 만일 cont가 오른쪽에 나타나면 포인터 그 자체가 상수이다. 양쪽에 모두 나타난다면 둘 다 상수이다.

포인터로 가리켜 지는 것이 상수 일 때 어떤 프로그래머들은 타입명 앞에 const를 나타내고 또 어떤 프로그래머들은 타입명 뒤에(하지만 *의 앞에) const를 나타낸다. 따라서, 다음 함수들은 같은 인자 타입을 취한다.

class Widget {... };

void f1(const Widget * pw); // f1은 상수 Widget 객체를 가리키는 포인터를 취함
void f2(Widget const * pw); // f2도 마찬가지

const 의 가장 강력한 사용법 중의 하나는 함수 선언에 적용하는 경우이다. 함수 선언시 const는 함수의 반환값, 개별 인자, 그리고 멤버 함수를 위해선 함수 전체에 적용할 수 있다.

const Rational operator * (const Rational lhs, const Rational & rhs);

왜 operator*의 결과가 const 객체이어야 하는가? 이것은 만일 그렇지 않았다면 클라이언트는 다음과 같은 큰 실수를 저지를 수 있기 때문이다.

Rational a, b, c;
...
(a * b) = c; // a*b의 곱으로 치환

좋은 사용자 정의 타입의 조건들 중의 하나는 내부 타입과의 이유없는 동작상의 비호환성을 피하는 것이며 두 숫자의 곱으로 치환을 허용할 이유가 없다고 본다. operator* 의 반환값을 const로 선언하는 것은 이런 것을 막아 준다.

const 인자들에 관해서는 특별하게 새로운 것이 없다. 그들은 로컬 const 객체와 같이 동작한다. 하지만, const 멤버 함수라면 얘기가 달라진다.

물론 const 멤버 함수의 목적은 어떤 멤버 함수들이 const 객체상에서 호출될 수 있는지를 나타내는 것이다. 하지만, 많은 사람들이 const 사용 여부만 차이가 나는 멤버 함수들이 오버로딩될 수 있다는 사실을 간과하고 있으며, 이것은 C++의 중요한 특징이다. String 클래스를 다시 한 번 고려해보자.

class String {
public:
...
// 비상 수 객체를 위한 operator[]
char & operator[] (int position) { return data[position]; }

// 상수 객체를 위한 operator[] (int position) const { return data[position]; }

private:
char * data;
};

String s1 = "Hello";
cout << s1[0]; // 비상수 String::operator[]를 호출

const String s2 = "World";
cout << s2[0]; // 상수 String::operator[]를 호출

operator[]를 오버리딩하고 각각의 버전에 서로 다른 반환값들을 부여함으로써 서로 다르게 처리되는 const 및 비 const String을 가질 수 있다.

String s = "Hello"; // 비상수 스트링 객체
cout << s[0]; // 양호, 비상수 String을 읽음
s[0] = 'x'; // 양호, 비상수 String을 씀

const String cs = "World"; // 상수 스트링 객체
cout << cs[0]; // 양호, 상수 String을 읽음
s[0] = 'x'; // 에러! 상수 스트링을 씀

그런데, 여기서 에러는 operator[]의 호출시 반환된 값과 함께 동작될 때에만 발생하고 있다. oeprator[] 자체의 호출은 모두 양호하다. 에러는 const char&로 치환을 시도할 때 발생한다. 왜냐하면, 이것은 operator[] 의 const 버전의 반환값이기 때문이다.

또한, 비const operator[]의 반환 타입이 char의 레퍼런스이어야 한다는 것을 주목한다. char 그 자체는 안된다. 만일 operator[]가 단순히 char를 반환한다면, 다음과 같은 문장은 컴파일 되지 않을 것이다.

s[0] = 'x';

그것은 내부 타입을 반환하는 함수의 반환값을 수정하는 것은 불법이기 때문이다.
만일 그것이 적법하다고 치더라도 C++가 값에 의해 객체를 반환한다는 사실은 s.data[0] 그 자체가 아닌 s.data[0]의 복사본이 수정될 것이라는 것을 의미하는 것이다. 어쨌든, 이것은 원하는 동작이 아니다.

멤버함수가 const라는 것이 정확히 의미하는 것이 무엇일까?

두 가지의 지배적인 견해가 있다. 그것은 비트 단위의 상수성(bitwise constness)과 개념적 상수성(conceptual constness)이 있다.

비트단위의 상수성

비트 단위의 const측은 멤버 함수가 어떠한 객체 데이터 멤버도 변경시키지 않는다면(static인 것을 제외하고) 즉, 멤버 함수가 객체 내부의 어떠한 비트도 변경시키지 않는다면 그 멤버 함수는 const이다.
비트 단위의 상수성의 장점은 위반사항을 검출하기가 용이하다는 것이다. 컴파일러는 단지 데이터 멤버로의 치환만을 검사한다. 실제로 비트 단위의 상수성은 C++의 상수성의 정의이며, const 멤버 함수가 호출된 객체의 어떠한 데이터 멤버도 변경시킬 수 없다.

불행히도 const로 동작하지 않는 많은 멤버 함수들이 비트 단위의 테스트를 거친다. 특히, 포인터가 기리키는 것을 변경시키는 멤버 함수는 자주 const로 동작하지 않는다. 하지만, 포인터가 객체 내에 있기만 하면 그 함수는 비트 단위의 const이며, 컴파일러는 불평하지 않을 것이다. 이것은 비직관적인 동작으로 이끌 수 있다.

class String{
public:
// 생성자는 데이터가 그 값이 가리키고 있는 것의 복사본을 가리키게 함
String(const char * value=0);

operator char * () const { return data; }

private:
char * data;
};

const String s = "Hello"; // 상수 객체 선언
char * nasty = s; // 연산자 char *() const를 호출
*nasty = 'M'; // s.data[0]을 수정
cout << s; // "Mello"를 씀

여기에서는 확실히 특정값으로 상수 객체를 생성하거나 그 객체상의 const 멤버 함수들을 호출할 때 뭔가 잘못된 것이 있다. 여전히 그 값은 변경될 수 있다!

개념적 상수성
이러한 사실은 개념적 상수성의 관점을 이끈다. 이 사상의 추종자들은 const 멤버 함수는 자신을 호출한 객체의 일부 비트들을 변경할 수 있다고 주장한다. 하지만 이는 클라이언트에 의해 검출될 수 없는 방식에 한해서이다. 예들 들어, String 클래스가 요청될 때마다 객체의 길이를 저장하기를 원할 수 있다.

class String {
public:
// 생성자는 데이터가 값이 가리키고 있는 것의 복사본을 가리키게 함
String (const char * value = 0) : lengthIsValid(0) { ... }

unsigned int length() const;

private:
char * data;

unsigned int dataLength; // 최근 계산된 스트링의 길이
bool lengthIsValid; // 길이가 현재 유효한지의 여부
};

unsigned int String::length() const
{
    if (!lengthIsValid) {
        dataLength = strlen(data); // 에러!
        lengthIsValid = true; // 에러!
    }

    return dataLength;
}

length의 이러한 구현은 확실히 비트 단위의 상수성이 아니다. (dataLength와 lengthIsValid는 모두 변경될 수 있다.) 하지만, const String 객체들을 위해선 정당해야 할 것으로 보인다. 컴파일러는 이에 동의하지 않는다. 컴파일러는 비트 단위의 상수성을 주장한다. 어떻게 해야 할까?

해법은 간단하다. 바로 이러한 경우를 위해서 C++ 표준화 위원회가 완전하게 제공하고 있는 const 관련 잠정적인 용어를 이용하는 것이다. 이것은 키워드 mutable을 이용한다. 비정적 데이터 멤버에 적용되엇을 때 mutable은 그 멤버들을 비트 단위 상수성의 제약으로부터 자유롭게 한다.

class String {
public:
.... // 이전과 동일
private:
char * data;

mutable unsigned int dataLength; // 이 데이터 멤버들은 이제 어디서든지 변경될 수 있다.

mutable bool lengthIsValid; // 심지어, 상수 멤버 함수 내부에서도 가능하다.
};

unsigned int String::length() const
{
    if(!lengthIsValid) {
        dataLength = strlen(data); // 이제 양호
        lengthIsValid = true; // 양호
   }

   return dataLength;
}

mutable은 비트 단위 상수성을 원하지 않는 경우를 위한 훌륭한 해법이다. 하지만, 그것은 표준화 과정에서 비교적 최근에 추가되었기 때문에 컴파일러에 따라서는 아직 이를 지원하지 않을 수도 있다. 만일 이러한 경우라면, C++에서 그러한 상수성을 버리는(cast) 다른 방법을 찾아야 할 것이다.

클래스 C의 멤버 함수 내부에서 this 포인터는 다음과 같이 선언된 것처럼 동작한다.

C * const this; // 비상수 멤버 함수를 위함

const C * const this; // 상수 멤버 함수를 위함

이와 같은 경우, const와 비const 객체 모두를 위해서 String::length의 미심쩍은 버전(즉, 컴파일러가 지원한다면 mutable로 해결할 수 있는 것)을 유효하게 만들기 위해선 this 의 타입을 const C * const에서 C * const로 변경하는 것이다. 이것을 직접적으로 할 수는 없지만, 로컬 포인터를 this가 가리키는 것과 같은 객체를 가리키도록 초기화함으로써 이를 속일 수 있다. 그런 다음 로컬 포인터를 통해 변경시키고자 하는 멤버에 접근할 수 있다.

unsigned int String;:length() const

{

           // this의 로컬 버전을 만듦. 여기서는 this const 포인터가 아님

           String * const localThis = const_cast<String * const>(this);

 

           if(!lengthIsValid) {

                     localThis->dataLength = strlen(data);

                     localThis->lengthIsValid = true;

           }

          

 

           return dataLength;

}


이것은 올바른 방법은 아니라고 생각되지만 쓰고 안쓰고는 프로그래머에게 전적으로 달려 있다.

상수성을 버린 것이 유용하고 안전한 경우가 있다. 그것은 const 객체를 비 const 인자를 취하는 함수로 전달하고자 하는 경우이며 인자는 함수 내에서 변경되지 않을 것이다. 이 두 번째 조건은 중요하다. 왜냐하면, 객체가 원래부터 const로 정의되어 있더라도 읽혀지기 만할(쓰여지지 않는) 객체의 상수성을 버리는 것은 항상 안전하기 때문이다.

예를 들어, 어떤 라이브러리에서는 strlen을 다음과 같이 잘못 선언한 것으로 알려져 있다.

int strlen(char * s);

여태껏 보아왔던 strlen라면 s가 가리키는 것을 확실히 변경시키지 않을 것이다. 하지만, 타입 const char *의 포인터에 대해 이를 호출하는 것은 적절하지 않다. 이 문제를 피하기 위해서 strlen으로 그러한 포인터를 전달할 때 다음과 같은 방법으로 그들의 상수성을 안전하게 버릴 수 있다.

const char * klingonGreeting = "nuqneH";

size_t length = strlen(const_cas<char*>(klingonGreeting));

호출되고 있는 함수(여기에서는 strlen)가 그 인자가 가리키고 있는 것을 변경시키려고만 하지 않는다면 올바른 동작이 보장될 것이다.

출처 : Effective C++, Scott Meyers 저