U E D R , A S I H C RSS

More EffectiveC++/Operator

1. Operator

1.1. Item 5: Be wary of user-defined conversion functions.

  • Item 5: 사용자 정의 형변환(conversion) 함수에 주의하라!

  • C++는 타입간의 암시적 type casting을 허용한다. 이건 C의 유산인데 예를 들자면 charint 에서 shortdouble 들이 아무런 문제없이 바뀌어 진다. 그런데 C++는 이것 보다 한수 더떠서 type casting시에 자료를 잃어 버리게 되는 int에서 short과 dougle에서 char의 변환까지 허용한다.

일단 이런 기본 변환에 대해서 개발자는 어찌 관여 할수 없다. 하지만, C++에서 class의 형변환은 개발자가 형변환에 관하여 관여할수 있다. 변환에 관하여 논한다.

  • C++에서는 크게 두가지 방식의 함수로 형변환을 컴파일러에게 수행 시키킨다:
    single-argument constructorsimplicit type conversion operators 이 그것이다.
  • single-argument constructors 은 인자를 하나의 인자만으로 세팅될수 있는 생성자이다. 여기 두가지의 예를 보자

~cpp 
class Name {
public:
    Name( const string& s);
    ...
};
class Rational {
public:
    Rational( int numerator = 0, int denominator = 1);
    ...
};

  • implicit type conversion operator 은 클래스로 하여금 해당 타입으로 return 을 원할때 암시적인 변화를 지원하기 위한 operator이다. 아래는 double로의 형변환을 위한 것이다.

~cpp 
class Rational{
public:
    ...
    operator double() const;
};

Rational r(1,2);
dougle d = 0.5 * r;
참 괜찮은 방법이다. 하지만 이 방법은 개발자가 의도하지 않은 형변환마져 시키는 것때문에 문제가 발생한다. 다음을 보자
~cpp 
Rational (1,2);
cout << r;      // should print "1/2"
operator<<는 처음 Raional 이라는 형에 대한 자신의 대응을 찾지만 없고, 이번에는 r을 operator<<가 처리할수 있는 형으로 변환시키려는 작업을 한다. 그러는 와중에 r은 double로 암시적 변환이 이루어 지고 결과 double 형으로 출력이 된다.

뭐 이런 암시적 형변환을 막을려면, 형전환 시키고 하는 암시적 사용을 하지 않고, 다른 함수로 명시적으로 해 줄수 있다.
~cpp 

class Raional{
public:
    ...
    double asDouble() const;
};

Rational r(1,2);
cout << r;                // error!
cout << r.asDouble();     // double로의 전환의 의도가 확실히 전해 진다.

이런 예로 C++ std library에 있는 string이 char*로 암시적 형변환이 없고 c_str의 명시적 형변환 시킨다.

  • single-argument constructor 는 더 어려운 문제를 제공한다. 게다가 이문제들은 암시적 형변환 보다 더 많은 부분을 차지하는 암시적 형변환에서 문제가 발생된다.

~cpp 
template<class T>
class Array{
public:
    Array ( int lowBound, int highBound );
    Array ( int size )

    T& operator[] (int index)
    ...
};

첫번째 생성자는 배열의 lowBound~highBound 사이로의 크기 제한자이고, 두번째 생성자는 해당 크기로 배열 공간 생성인데, 이 두번째의 생성자가 형변환을 가능하게 만들어서 무한한 삽질에 세계에 당신을 초대한다. (실제로 이런 의미로 써있다. --상민)

예를 들어서 다음을 보자
~cpp 
bool operator==( const Array< int >& lhs, const Array<int>& rhs);

Array<int> a(10);
Array<int> b(10);
...
for ( int i = 0; i<10; ++i)
    if( a == b[i] ) {           // 헉스! 이런 "a"는 "a[i]" 써야 할 코드였다!. (개발자의 실수 의미, 한미 양국에서 같은 발음의 oops! --;;  --상민)
        a[i]와 b[i]가 같을때 이 코드를 수행한다.;
    }
    else {
        위의 조건을 만족하지 못하면 이 코드를 수행한다.;
    }
7줄 if ( a == bi ) 부분의 코드에서 프로그래머는 자신의 의도와는 다른 코드를 작성했다. 이런 문법 잘못은 당연히! 컴파일러가 알려줘야 개발자의 시간을 아낄수 있으리, 하지만 이런 예제가 꼭 그렇지만은 않다. 이 코드는 컴파일러 입장에서 보면 옳은 코드가 될수 있는 것이다. 바로 Array class에서 정의 하고 있는 single-argument constructor 에 의하여 컴파일시 이런 코드로의 변환의 가능성이 있다.

~cpp 
for ( int i = 0; i < 10; ++i)
    if ( a == static_cast< Array<int> >(b[i]) )...
bi 는 int형을 반환하기 때문에 이렇게 즉석에서 맞춤 생성자로 type casting(형변환)을 컴파일러가 암시적으로 해줄수 있다. 이제 사태의 심각성을 알겠는가?

  • explicit
이런 애매한 상황을 피할수 있는 가장 효과적인 방법은 C++에서 등장한 새로운 키워드인 explicit 의 사용이다. 이 키워드가 붙은 생성자로의 형변환에서는 반드시 명시적인 선언이 있어야 가능하다. 즉 위의 코드를 다시 작성하여 explicit의 사용을 알아보고 문법상 유의 사항도 알아 보자

~cpp 
template<class T>
class Array{
public:
    ...
    expicit Array ( int size )   
    ...
};

Array<int> a (10); 
Array<int> b (10);       // 두개 모두 생성 가능하다

if ( a == Array<int>(b[p])) ...                  // 이 코드 역시 올바르다. 
                                                 // 하지만 다른 개발자가 해석에 사용자의 의도가 약간 의문이 간다.
if ( a == static_cast< Array<int> >(b[p])) ...   // 맞다. 위와 마찬가지로 다른 사람이 땀흘린다
if ( a == (Array<int>) b[i] )                    // C-style 의 형변환인데 역시나 다른사람이 열받는다.
이렇게 explicit를 사용하면 명시적으로만 형변환이 가능하다. 하지만 또 하나 문법상 유의 해야 할사항은
~cpp 
static_cast< Array<int> >(b[p])
이 구분에서 > > 이 두개를 붙여쓰면 >> operator로 해석하니 유의해라

여기에 한가지 더 생각해 보자.

만약 당신의 컴파일러가 엄청 낡은 거거나 제작자가 깜빡해서, 혹은 explicit를 죽어도 넣기 싫다고 할때에는?

여기에 나온 Array 예제를 한번 고쳐서 마치 explicit 를 쓴것처럼 해보자
~cpp 
template<class T>
class Array{
public:
    class ArraySize{
    public:
        ArraySize(int numElements):theSize(numElements){}
        int size() const { return theSize; }
    private:
        int theSize;
    }
    Array( int lowBound, int highBound);
    Array( ArraySize size );                      // 새로운 선언!
    ...
}

이렇게 Array 안쪽에 ArraySize 를 선언하고 public으로 불어서 이렇게 생성하면
~cpp 
Array<int> a(10);

컴파일러 단에서 a생성자인 Array( ArraySize size) 에서 ArraySize 로의 single-argument constructor를 호출하기 때문에 선언이 가능하지만
~cpp 
bool operator==( const Array< int >& lhs, const Array<int>& rhs);

Array<int> a(10);
Array<int> b(10);
...
for ( int i = 0; i<10; ++i)
    if( a == b[i] ) 
이런 경우에 operator==의 오른쪽에 있는 인자는 int가 single-argument constructor에 거칠수 없기 때문에 에러를 밷어 낸다.

*후기:이번껀 너무 길다. 다른거에 두배에 해당하는거 같은데 다음부터는 딴청 피우지 말고 해야지 --상민

1.2. Item 6: Distinguish between prefix and postfix forms of increment and decrement operators.

  • Item 6: prefix와 postfix로의 증감 연산자 구분하라!

우리는 ++와 --연산자(이하 가칭 가감 연산자)를 즐겨 쓴다. 이 연산자 역시 클래스에서 정의해서 사용할수 있다.
하지만 이 가감 연산자는 두가지로 나뉜다는 사실을 생각하면 갑자기 난감해 진다. 설마 설계자가 그런 단!순!한! 문제를 간과할리 없다.

~cpp 
class UPInt{  //무한 정수형
public:
    UPInt& opertrator++(int);         // prefix++
    const UPInt operator++(int);      // postfix++

    UPInt& opertrator--(int);         // prefix--
    const UPInt operator--(int);      // postfix--

    UPInt& operator+=(int);           // a += operator 는 UPInt와 ints 추가 위해
...
};
UPInt i;

++i;        // i.operator++();
i++;        // i.operator++(0);

--i;        // i.operator--();
i--;        // i.operator--(0);

보고 있자면 정말 아이디어 괜찮은듯 그럼 구체적인 구현부에 관해서 간단히 논한다.
~cpp 
// prefix 증가 연산자 부분과 fetch
UPInt& UPInt::operator++()
{
    *this += 1;       // 증가
    return *this      // fetch!
}

// postfix 증가 연산자 부분과 fetch
const UPInt& UPInt::operator++(int)
{
    UPInt oldValue = *this;             // fetch
    ++(*this);                          // prefix 증가 시킨다.
    return oldValue;
}

*작성자 사설: 아 나는 정말 이런 리턴이 이해가 안간다. 참조로 넘겨 버리면 대체 컴파일러는 어느 시점에서 oldValue의 파괴를 하냔 말이다. C++이 reference counting으로 자원 관리를 따로 해주는 것도 아닌대 말이다. 1학년때 부터의 고민이단 말이다. 좀 명쾌한 설명을 누가 해줬으면..

암튼 저 위와 같이 하면 이해가 갈것이다. 하지만 이럴 경우 요 짓거리를 못한다.

~cpp 
    UPint i;
    i++++;                 
이 의미는
~cpp 
    i.operator++(0).operator++(0);
과 같다. 당연히 안되겠지?

  • 작성자 사설:본문에서는 그 뒤부터는 아예 이런걸 쓰지 말자는 필요성의 언급니다. 차후 추가의 필요성이 있을때 추가합니다.
    그냥 i++ 두번 쓰고 말지..

1.3. Item 7: Never overload &&, ||, or ,.

  • Item 7: 절대로! &&, ||, ',' 이 연산자들을 overload 하지 말아라


~cpp 
    char *p;
    ...
    if ((p != 0) && (strlen(p) > 10) ) ...
위의 코드에서는 strlen() 함수내부에서 p에 관련한 null pointer 검사가 필요하지 않다. 왜냐하면 && 에서는 앞의 조건이 부정 즉, ( false && anything ) 의 경우에는 뒤의 조건(anything)은 수행조차 안하기 때문이다. operator ||의 경우도 특정 조건에서,(true || anything) 뒤에 코드를 수행하지 않은다는 것은 비슷하다.
~cpp 
    int rangeCheck( int index)
    {
        if ((index < lowerBound) || (index > upperBound)) ...
        ...
    }

수많은 개발자들이 이런 단순한 원리를 프로그램 상에서의 짧은 진행(short-circuit)을 추구하는데 사용하였다. 그렇다면 C++에서의 객체들에게 operator ||, && 를 overload 시키면 짧은 진행을 추구하는데 도움이 되지 않을까? 그런데 하지 말라니 왜일까?

일단 operator &&, ||는 이렇게 두가지의 경우로 둘릴수 있다.
~cpp 
    if (expression1 && expression2) ...
이 문장의 해석을 컴파일러는 이렇게 받아 들인다.
~cpp 
    1. if (expression1.operator&&(expression2))...   // when operator && is member function.
    2. if (operator&&(expression1 , expression2))... // global function

자 이 두경우 모두를 생각해 보면 1,2 양쪽 다 expression1, expression2 의 결과 값이 필요한 상황이다. 즉, operator && 나 operator || 의 경우 양쪽이 class인자든, 어떤 형태이든 반드시 결과 값이 필요하다. 위에도 언급했지만, 이미 많은 개발자들이 &&와 ||의 특성을 잘 알고 사용하고 있으며, operator &&, ||의 overload는 구동되지 말아야할 코드가 구동되는 의도하지 않은 오류가 발생 소지가 있다.

"comma operator" 역시 마찬가지다. comma operator가 대체 뭐냐고?

comma operator는 표현(form expression)에 사용된다. 아래를 보자
~cpp 
void reverse(char s[])
{
    for (int i = 0, j = strlen(s)-1;
        i < j;
        ++i, --j)                   // 이녀석들이 바로 comma operator들이다. 기가 막혀..
    {
        int c = s[i];
        s[i] = s[j];
        s[j] = c;
    }
}
결론은 overload로 언어상의 이녀석들 원래의 능력 발휘하기가 힘들다. 이런 위험은 감수할 필요 없지 않은가? 참고로 다음을 알자

overload할수 없는 operator
~cpp 
    .                .*                ::                ?:
    new              delete            sizeof            typeid
    static_cast      dynamic_cast      const_cast        reinterpret_cast
overlod 할수 있는 operator
~cpp 
    operator new     operator delete
    operator new[]   operator delete[]
    +    -    *    /    %    ^    &    |    ~
    !    =    >    <    +=   -=   *=   /=   &=
    ^=   &=   |=   <<   >>   >>=  <<=  ==   !=
    <=   >=   &&   ||   ++   --   ,    ->*  ->
    ()   []

1.4. Item 8: Understand the differend meanings of new and delete

  • Item 8: new와 delete가 쓰임에 따른 의미들의 차이를 이해하라.

보통 C++에서 용어들을 정확히 이해 못할 경우가 있다. 바로 newoperator와 operator new가 그 대표적인 예가 될수있을 것이다. 다음의 코드를 보자
~cpp 
    string *ps = new string("Memory Management");

이 코드는 new operator를 사용한 것이다. new operator는 sizeof 처럼 언어 상에 포함되어 있으며, 개발자가 더 이상 그 의미의 변경이 불가능하다. 이건 두가지의 역할을 하는데, 첫째로 해당 객체가 들어갈 만한 메모리를 할당하는 것이고, 둘째로 해당 객체의 생성자를 불러주는 역할이다. new operator는 항상 이 두가지의 의미라 작동하며 앞에서 언급한듯 변경은 불가능하다.

보통 operator new는 이렇게 선언되어 있다.
~cpp 
    void * operator new(size_t size);
이건 과거 C에서의 malloc처럼 초기화 되지 않은 size만큼의 메모리를 할당해서 그걸 가리키는 void형의 pointer를 돌려주는 것이라고 예측할수 있겠다.(맞다) 개발자는 operator new를 overload할수 있지만 첫번째 인자는 항상 size_t가 되어야 한다.

아마 여러분은 operator new를 직접 부르는걸 결코 원하지 않겠지만(생성자, 파괴자를 생각해 보면 말이지), 써먹을수 있는데 한번 해보자.
~cpp 
    void *rawMemory = operator new(sizeof(string));

string 객체가 필요로 하는 메모리 만큼이 할당된다. 하지만 위의 의미처럼 malloc, operator new는 생성자를 호출하지는 앖는다. 생성자의 호출은 new operator 몫이다.

new operator를 보면
~cpp 
    string *ps = new string("Memory Management");
다음과 같은 코드는 컴파일러 단에서 이렇게 교체된다고 볼수 있다.
~cpp 
    void *memory = operator new(sizeof(string));

    call string::string("Memory Management") on *memory;

    string *ps = static_cast<string*>(memory);

해당 코드 두번째 부분의 생성자 호출을 눈여겨 봐라.

  • Placement new
당신은 생성자를 직접 호출하기를 원할때가 있을 것이다. 하지만 생성자는 객체(object)를 초기화 시키고, 객제(object)는 오직 처음 주어지는 단 한번의 값으로 초기화 되어 질수있기 때문에 (예-const 인수들 초기화에 초기화 리스트를 쓰는 경우) 생성자를 이미 존재하고 있는 객체에 호출한다는건 생각의 여지가 없을 것이다.

그렇지만 여러분이 raw memory로 객체를 할당한다면 초기화 루틴이 필요하다.

(여담-후후 나도 상단의 operator new를 보는 순간 이 생각했다.)

바로 operator new의 특화 버전인 placement new로 할수 있다.

다음은 placement new를 사용한 예제이다.
~cpp 
    cass Widget {
        public:
            Widget(int widgetSize);
            ...
    };
    Widget* constructWidgetInBuffer(void * buffer, int widgetSize)
    {
        return new(buffer) Widget(widgetSize);
    }

해당 함수(construcWidgetInBuffer())는 버퍼에 만들어진 Widget 객체의 포인터를 반환하여 준다. 이렇게 호출할 경우 객체를 임의 위치에 할당할수 있는 능력 때문에 shared memory나 memory-mapped I/O 구현에 용이하다 constructorWidget에 보이는건 바로
~cpp 
    return new (buffer) Widget(widgetSize);
이 문인데 , 아마 처음 보기에 생소할 것이다. 자세히 뜯어 보면, (buffer)에 의해서 암시적으로 new는 operator new로 호출되어 진다. 덧붙여, 아래의 void*는 메모리 상의 위치를 size_t는 메모리상 객체가 차지하는 영역이다. 자, 위와 비교해 보라 이 operator는 new를 overload한 버전이다.
~cpp 
    void* operator new(size_t, void *location)
    {
        return location;
    }

이거 간단히 보이지만 placement new의 전부이다. operator new의 역할은 해당 객체를 위한 메모리를 찾고(할당), 해당 포인터의 반환이고 placement new의 경우에는 호출자가 이미 메모리를 확보하였고, 단순히 포인터 반환만 해준다. 모든 placement new가 반드시 이런 pointer의 전달 역할을 한다. 그리고 size_t 인자가 아무런 이름이 없어도 반항 안한다. 자세한건 Item 6을 보면 이해가 갈것이다.

이런 placement new는 C++ 표준 라이브러리의 한 부분으로 placement new를 쓰고자 한다면 #include<new> 를 해주면 된다.

  • new 결론!
자자 new 결론이다.

the new operator : heap 메모리 확보와 생성자 호출 동시에

operator new : 해당 객체의 메모리 확보만

placement new : 이미 확보 메모리가 존재하고 객체 초기화를 원한다면

이다 어설프게 정리했고 차후 추가 할것이다.

  • Deletion and Memory Deallocation
동적 할당에 대하여 리소스 새는걸 방지할려면 해당 동적 할당에 상응하는 응징(?)이 필요할 것이다. 예를들어서 new나 operator new에는 이런거
~cpp 
    string *ps;
    ...
    delete ps;      // delete operator 를 사용한다.
당신이 쓰고 있는 컴파일러는 객체 ps point를 파괴하고 객체가 가지고 있는 메모리를 해제한다.

메모리 해제(deallocaion)은 operator delete함수에 행해지는데 일반적으로 이렇게 선언되어 있다.
~cpp 
    void operator delete(void * memoryToBeDeallocated);
그러므로
~cpp 
    delete ps;
는 이런 코드가 수행된다고 보면 된다.
~cpp 
    ps->~string();
    operator delete(ps);
(작성자 주: 이걸로 new와 delete의 환상,신비성이 깨졌다. )

그리고 이것의 의미는 당신이 초기화 되지 않은 raw로의 사용의 경우에는 new와 delete operator로 그냥 넘겨야 한다는 의미가 된다. 즉 이코드 대신에 다음의 예를 볼수 있을 것이다.
~cpp 
    void * buffer = operator new(50*iszeof(char));
    ...
    operator delete(buffer);
위의 코드는 C++상에서 malloc 과 free 를 호출 것과 동일한 역할을 한다.

그렇다면, 이번에 당신은 placement new를 사용해서 메모리상에 객체를 만들었다면 delete를 사용할수 없을 꺼라는 예측을 할수 있을 것이다. 자 다음 코드를 보면서 명시적인 delete를 행하는 코드들을 보자
~cpp 
    void * mallocShared(size_t size);
    void freeShared(void *memory);

    void *shareMemory = mallocShared(sizeof(Widget));
    Widget *pw = constructWidgetInBuffer(shareMemory, 10);  // placement new이닷!

    ...
    delete pw;  // 이렇게 해서는 안된다. sharedMemory는 mallocShared에서 할당되는데 어찌 할수 있으리요.
                // 그렇다면 다음과 같은 코드로 해야 한다.

    pw->~Widget();  // 먼저 임의로 파괴자를 호출한다. 이렇게 하면 pw과 관련된 자원을 반환할것이고.
    freeShared(pw); // 최종적으로 pw가 할당된 메모리 영역을 해제 시킨다.

  • Arrays
별다른 특별한 지적사항이 없다. 여태 까지의 연장선이고 단 new를 이용한 배열 할당을 삭제할 경우
~cpp 
    string *ps = new string[10];
    delete [] ps;
와 같이 짝을 맞추어 주어야 한다. placement는 당근 위의 지시 사항에 따르는 것이고 operator delete는 item 6에서와 같이 for문으로 순회하면서 지워 주어야 한다.

마지막으로 여기서 보다 시피 new와 delete를 만드는 자체는 당신이 조정 할수 없는 영역에 존재하지만 메모리 할당은 당신의 손아래 있다. new와 delete를 최적화나 수정 할때 꼭 기억해라 당신이 정말로 그걸 할수 없는가에 관해서 말이다. 당신은 그것들의 방법(new,delete메모리 할당 방법)은 변경할수 있다. 그러나 그들은 언어에 의해서 규정되어 져 있는 영역이다.


Valid XHTML 1.0! Valid CSS! powered by MoniWiki
last modified 2021-02-07 05:23:49
Processing time 0.0415 sec