'3. Implementation/C / C++'에 해당되는 글 23건

  1. 2008.08.24 가능한 const를 이용한다.
  2. 2008.08.24 #define 보다는 const와 inline을 사용하라
  3. 2008.08.11 Initializing Static Members (C++ 에서 정적 멤버 초기화)
2008. 8. 24. 22:50

가능한 const를 이용한다.

포인터를 위해선 포인터 그 자체, 포인터가 가리키는 데이터, 또는 이 모두를 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 저
2008. 8. 24. 21:40

#define 보다는 const와 inline을 사용하라

#define ASPECT_RATIO 1.653

와 같이 전처리기 매크로를 사용하는 대신 상수를 정의한다.

const double ASPECT_RATIO = 1.653;

이 접근 방법에서 두 가지의 특별한 경우

1. 상수 포인터를 정의하기기 다소 까다로울 수 있다.

예로, 헤더 파일 내에서 const char * 기반의 스트링을 정의하기 위해선 const 를 두 번 이용해야 한다.

const char * const authorName = "Scott Meyers";

2. 클래스 범위의 상수를 정의하는 것이 편리할 때가 있다.

class GamePlayer {
private:
static const int NUM_TURNS=5; // 상수 선언
int scores[NUM_TURNS]; // 상수의 사용
....
};

하지만 위에서 보는 것은 NUM_TURNS을 위한 선언언(declaration)이지 정의(definition)가 아니라는 단점이 있다. 즉, 구현 파일에 정적 클래스 멤버를 정의해야 한다.

const int GamePlayer::NUM_TURNS; // 필수적인 정의, 클래스 구현 파일에 들어간다.

오래된 컴파일러들은 이러한 구문을 이해하지 못할 수 있다. (예로 Embeded Visual C++ 4.0 )
과거에는 선언의 시점에서 정적 클래스 멤버에 초기값을 할당하는 것이 불법이었다.

위 구문을 사용할 수 없을 경우에는 초기값을 정의 시점에 넣을 수 있다.

Class EngineeringConstans {
private:
static const double FUDGE_FACTOR;
...
};

// this goes in the class implementation file
const double EngineeringConstans::FUDGE_FACTOR = 1.35;

유일한 예외는 클래스의 컴파일시 클래스 상수의 값을 필요로할 때이다. 예를 들어, 앞에서 GamePlayer::scores의 선언에서 컴파일러는 컴파일 시기에 배열의 크기를 알아야 한다. 클래스 내 정수형 클래스 상수에 대해 초기값 지정을 금지하는 컴파일러는 보완하기 위한 일반적인 방법은 enum을 이용하는 것이다. ("enum hack"이라고 불림)

class GamePlayer {
private:
enum { NUM_TRUNS = 5 };

int scores[NUM_TURNS];
...
};

#define 디렉티브에 관한 또다른 하나의 요옹 예는 함수와 같은 매크로를 구현하기 위해서이다.

#define max(a,b) ((a) > (b) ? (a) : (b))

이와 같은 매크로를 작성할 때는 언제나 매크로 몸체의 모든 인자들을 괄호로 묶어야 한다는 것을 기억해야 한다. 그렇지 않으면 누군가 그 매크로를 호출하였을 때 문제가 발생할 수 있다.

하지만 이를 올바르게 적용했다고 할지라도 발생할 수 있는 신기한 상황을 살펴보자.

int a = 5, b = 0;
max(++a, b); // a가 두번 증가한다.
max(++a, b+10); // a가 한번 증가한다.

이와 같은 상황이 발생하지 않도록 하려면 inline 함수를 이용함으로써 매크로의 모든 효율성, 모든 예측 동작 및 정규 함수의 타입 안정성을 취할 수 있다.

inline int max(int a, int b) { return a > b ? a : b; }

이 버전의 max는 int로만 호출될 수 있기 때문에 위의 매크로와 사뭇 다르다. 하지만, 템플릿은 그 문제를 깨끗하게 해결한다.

template <class T>
inline const T& max(const T& a, const T& b)
{ return a > b ? a : b; }

그런데 max와 같은 일반적으로 이용되는 함수들을 위한 템플릿을 작성하는 것을 고려하기 전에 그들이 이미 존재하는지를 알아보기 위해 표준 라이브러리를 살펴 보아야 한다. max의 경우 표준 C++ 라이브러리에 포함되어 있다.

const 와 inline의 유용성으로 전처리기의 필요성이 감소하였지만 완전히 제거 되지는 않는다. 아직 컴파일러를 컨트롤하는 데 여전히 중요한 역할을 차지하고 있다. 아직 전처리기를 명퇴시킬 시기는 아니지만 더 길고 자주 휴가를 주기 시작할 것을 계획 해야 한다.

출처 : Effecitve C++, Scott Meyers 저
2008. 8. 11. 11:28

Initializing Static Members (C++ 에서 정적 멤버 초기화)

Static member initialization occurs in class scope. Therefore, they can access other member data or functions. For example:

// initializing_static_members.cpp
class DialogWindow
{
public:
static short  GetTextHeight()
    {
return 1;
    };
private:
static short nTextHeight;
};
short DialogWindow :: nTextHeight = GetTextHeight();
int main()
{
}

Note that in the preceding definition of the static member nTextHeight, GetTextHeight is implicitly known to be DialogWindow :: GetTextHeight.

출처 : http://msdn.microsoft.com/en-us/library/8yc35419(VS.80).aspx