복사(copy)는 컨테이너 안의 객체에 맞게 비용은 최소화하며, 동적은 정확하게 하자

일반적으로 STL에 넣는 동작(push_back, insert) 이나 값을 얻어오는 동작은 모두 객체의 복사 를 통해서 이루어진다. 이 동작을 관리하는 연산자 두가지는 복사생성자, 대입연산자 이며 일반적으로

class Widget{
public:

Widget(const Widget& );
Widget& operator=(const Widget&);
}
형식으로 선언된다. 이 연산자들의 경우는 클래스에 대해 구현이 안되있을 경우에는 컴파일러가 자동적으로 생성해서 사용하게 된다. 이것과 관련된 내용으로 RuleOfThree 를 참조하는것이 좋을것 이다.
컨테이너의 동작이 객체를 복사하는 것이 기본이기 때문에 상속받아 구현한 클래스를 부모 클래스를 넣는 컨테이너에 넣으려하면 슬라이스 라는 현상이 발생하며 이것은 상속받은 클래스의 맴버함수나 가상함수를 호출하는데 문제를 일으키게 된다.
이것을 해결하는 방법은 포인터의 컨테이너를(물론 상위클래스의 포인터) 사용하는것 이지만 이 방법은 포인터의 객체 할당과 해제에 대해 신경을 써줘야 한다.

적재적소에 알맞은 컨테이너를 사용하자


  • 표준 STL 시퀀스 컨테이너 : vector. string, deque, list
  • 표준 STL 연관 컨테이너 : set, multiset, map multimap
  • 비표준 시퀀스 컨테이너 : slist(싱글링크드리스트), rope(대용량 string)
  • string 대산 사용되는 vector<char> : 가끔사용. 항목13참조.
  • STL 에속하지 않는 표준 컨테이너 : 배열, bitset, valarray, stack, queue, priorty_queue

  • 연속메모리 컨테이너 : list를 제외한 STL 시퀀스 컨테이너
  • 노드기반 컨테이너 : STL 연관 컨테이너, list





정리

  • 임의정렬을 사용할려면 연관컨테이너는 안된다.
  • 삽입이나 삭제시 컨테이너의 요소들이 밀려나는 일이 없어야 한다면 연속메모리 컨테이너는 안된다.
  • 배열과의 호환을 원하면 vector
  • 탐색속도가 중요하다면 해쉬컨테이너 -> 정렬된 백터 -> 연관 컨테이너 를 고려한다.
  • string 은 라이브러리 마다 참조카운터를 사용한 경우와 아닌 경우가 있으며 참조카운터 모드를 조절할수 있는 경우도 있다. 대용량 string인 rope 는 보통 참조카운팅을 이용한다.
  • 삽입과 삭제의 트랜잭션적 의미를 부여할려면 list
  • 반복자, 포인터, 참조자 무효화를 최소한 으로 하기 위해서는 노드기반 컨테이너.

템플릿 프로그래밍 : 함수자

아 이제 함수자를 이용하거나 구현하는건 깨달아버린 느낌이다.


/// 레이어를 이용해 객체들을 관리하는 템플릿 클래스
template<class T, class Tr = ContainerObjectReleaser<T> >
class ContainerTmpl
{
public:
    typedef std::vector
<T> Layer;    
    typedef std::map
< int, Layer > Container;

private:
    Container    _container;

public:
    ContainerTmpl();
   
virtual ~ContainerTmpl();

    void    Release();
   
void    ReleaseLayer(int key);

    template <class D>
   
bool    DoAll(D d);

    template <class D>
   
bool    DoLayer(int key, D d);
};

#include Container.inl



대략 이런 느낌의 템플릿 코드를 만들고 컨테이너에 담아넣은 오브젝트들을 해제하는 함수자를 이런식으로 구현


/// PrimitiveMesh를 해제시키는 함수자
struct ReleaserPrimitiveMesh
{
public:
   
bool operator()(lgn::PrimitiveMesh* p)
    {
        p
->Release();
        delete p;
       
return true;
    }
};



그리고 선언하고 사용하다가 Release() 를 호출하면 위의 함수자를 이용해서 객체들을 해제하도록 한다.


ContainerTmpl<lgn::PrimitiveMesh*, ReleaserPrimitiveMesh>    _containerRender;

_containerRender.Release();

아니면 ContainerTmpl 의 DoAll 함수에 함수자를 직접 지정해서 사용하도록 하던지.
우선 함수자를 먼저 구현하고…


struct RenderPrimitiveMesh
{
private:
    lgn::Renderor
* _r;

public:
    RenderPrimitiveMesh( lgn::Renderor
& r )
        : _r(
&r)
    {
    }

    bool operator()(lgn::PrimitiveMesh* p)
    {
        p
->Render(*_r);
       
return true;
    }
};


그리고 필요에 따라 DoAll 함수를 호출한다.


_containerRender.DoAll( RenderPrimitiveMesh(_r) );

DoAll 의 내부 코딩은 대략 이런식


template <class T, class Tr>
template <class D>
bool ContainerTmpl<T, Tr>::DoAll(D d)
{
    Container::iterator end
= _container.end();
   
for( Container::iterator i = _container.begin(); i != end; ++i )
    {
        DoLayer( i
->first, d );
    }
   
return true;
}

template <class T, class Tr>
template
<class D>
bool ContainerTmpl<T, Tr>::DoLayer(int key, D d)
{
    Layer
* layer = GetLayer(key);

    Layer::iterator i = layer->begin();
    Layer::iterator end
= layer->end();
   
while( i != end )
    {
       
if( d( *i ) == false )
           
throw Exception;
       
++i;
    }
   
return true;
}


이거 꽤 재미있는데. 나중에 이런식으로 잘되어있는거 코딩할때는 상당히 재미있겠군. 내가 저런 형태로 라이브러리를 만든다면 대략 토나오는 느낌이겠지만.
예전엔 상당히 아리까리 했는데 오늘은 꽤나 명확한 느낌으로 다가온다.

관련내용.


  • EffectiveSTL38 – 함수자 클래스는 값으로 전달되도록 설계하자.
  • EffectiveSTL39 – 술어 구문은 순수 함수로 만들자.
  • EffectiveSTL40 – 함수자 클래스는 어댑터 적용이 가능하게(adaptable) 만들자.
  • EffectiveSTL41 – ptr_fun, mem_fun, mem_fun_ref 의 존재에는 분명한 이유가 있다.
  • EffectiveSTL42 – less<T>는 operator< 의 의미임을 꼭 알아두자.

    외 링크에 적혀있는 내용의 출처는 Effective STL.

  • 전처리기 오퍼레이터 정리

    Stringizing Operator (#)
    #define stringer( x ) printf( #x “\n” )

    int main()
    {
        stringer( In quotes in the printf function call\n );
        stringer( “In quotes when printed to the screen”\n );   
        stringer( “This: \”  prints an escaped double quote” );
    }

    이런식으로 매크로를 사용할 경우 전처리를 거치면
     

    int main()
    {
       printf( “In quotes in the printf function call\n \n );
       printf( \”In quotes when printed to the screen\”\n \n );
       printf( \”This: \\\” prints an escaped double quote\” \n );
    }

    이런 코드로 바뀌고

    In quotes in the printf function call

    “In quotes when printed to the screen”

    “This: \” prints an escaped double quotation mark”

    결과는 이렇다.

    Charizing Operator (#@)

    #define makechar(x)  #@x

    a = makechar(b);

    이렇게 정의하고 사용을 하면

    a = ‘b’;

    이런 의미가 된다.

    Token-Pasting Operator (##)

    #define paster( n ) printf( “token” #n ” = %d”, token##n )

    int token9 = 9;
    paster( 9 );

    이렇게 하면

    printf( “token” “9” ” = %d”, token9 );

    이렇게 바뀌고 결돠적으로

    printf( “token9 = %d”, token9 );

    이렇게 된다.

    정리 참고는 MSDN

    정리한 이유.

    근래에 코드 정리를 하다가 로그를 남기는 부분을 보게되었다.

    #ifdef _DEBUG
      LOG(“블라블라블라”);
    #endif

    이런식으로 되어 있는데 메크로를 잘 이용하면 저렇게 #ifdef 블럭으로 감싸지 않고도 자동으로 릴리즈바이너리에 포함되지 않도록 할수 있다.

    간단하게

    #ifdef _DEBUG
      #define LOG(x)    writeLog(x)
    #else
      #define LOG(x)
    #endif

    이렇게 만들수도 있다.
    LOG가 메크로고 실제 쓰는 함수는 writeLog일때 릴리즈 모드일때는 아예 writeLog 코드는 사라져 버린다. 하지만 이건 인자가 하나일 경우에만 가능하다. 가변인자(printf같은것)를 사용하는 로그함수일 경우에는 약간더 복잡해 지는데 C99 표준이 지원되는 컴파일러일 경우에는

    #ifdef _DEBUG
      #define LOG(fmt, …) writeLog(fmt, __VA_ARGS__)
    #else
      #define LOG(fmt, …)
    #endif

    이렇게 하면 전처리기에서도 가변인자를 처리할수 있고 보는것 처럼 릴리즈 모드에서는 소스에 포함되 않게도 할수 있다.

    하지만… 비주얼스튜디오 2003 이하는 __VA_ARGS__ 라는 메크로를 지원하지 않는다.
    현재로서는 가장 최신인 2005만 지원하고 있다. 물론 저게 없다고 가변인자를 사용하는 함수들을 메크로로 치환시키지 못하는것은 아니다.

    #ifdef _DEBUG
      #define LOG writeLog
    #else
      #define LOG
    #endif

    void writeLog(char* fmt, …);

    이렇게 하면 분명 디버그 모드일때는 LOG가 writeLog로 치환되니

    LOG(“%s = %d”, szName, value);

    이것이

    writeLog(“%s = %d”, szName, value);

    이렇게 정상치환된다.(물론 당연한 메크로의 기능일 뿐이다.)

    하지만 릴리즈 모드에서는?

    (“%s = %d”, szName, value);

    …이런 코드가 덩그러니 남아버리게 된다.

    어떻게 하면 저 덩그러니 남는걸 없앨수 있을까를 고민하다가 어디선가 찾아낸 방법.
    바로 Token-Pasting Operator (##) 를 사용하는 방법이다.
    (GPG에서 누군가 올렸었던것. 고민중에 돈오하듯이 깨우침.)

    #ifdef _DEBUG
      #define LOG writeLog
    #else
      #define LOG /##/
    #endif

    void writeLog(char* fmt, …);

    이렇게 해놓으면 릴리즈 모드에서 LOG를 사용한 코드는

    //(“%s = %d”, szName, value);

    이렇게 치환이 되고 컴파일 과정에서 완벽하게 사라진다.

    아싸 조쿠나.

    객체 생성 및 소멸 과정 중에는 절대로 가상 함수를 호출하지 말자

    출처 : Effective C++. 항목 : 객체 생성 및 소멸 과정 중에는 절대로 가상 함수를 호출하지 말자.

    특정 기반클래스를 상속받은 자식클래스의 객체를 생성할때(동적할당이든 정적할당이든) 기반클래스에 가상함수를 선언하고 기반 클래스의 생성자에서 그 가상함수를 호출하는 경우 아무리 자식클래스에서 가상함수를 재구현 했다고 하더라도 기반 클래스의 생성자에서 호출되는 것은 기반 클래스의 가상함수이다.

    이것은 C++ 생성자 호출순서와 관련이 있는데 기반 클래스가 생성된 이후 자식 클래스가 생성되기 때문에 기반 클래스의 생성자 시점에서 가상함수를 호출하면 그때는 자식 클래스가 생성이 안된 시점이기 때문이다.

    가상함수 뿐만 아니라 typeid, dynamic_cast 등의 런타임 타입정보를 사용하는 모든 동작에서 기반클래스의 생성자안은 기반클래스의 타입으로 동작하게 된다.
    일반적으로 원하는 다형성으로써의 동작은 하지 않으니 이런 경우는 피해야 한다.

    다형성을 가진 기본클래스에서는 소멸자를 반드시 가상 소멸자로 선언하자.

    출처 : Effective C++. 항목 7 : 다형성을 가진 기본클래스에서는 소멸자를 반드시 가상 소멸자로 선언하자.

    상속을 사용한 객체를 할당하고 해제할때 기반클래스의 포인터를 사용하여 할당 및 해제를 사용하는 경우가 무척 많다. 이 경우 기반 클래스 및 자식 클래스의 소멸자가 virtual 이 붙어야 하는 경우와 붙지 말아야 하는 경우가 있다.
    class Base {
    // …
    };

    class Derived : public Base {
    // …
    };

    Base* object = new Derived();
    // object 사용
    delete object;

    위와 같은 코드가 있을 경우 Base 의 포인터를 이용해 할당을 하고 Base형의 포인터를 이용해 메모리를 해제한다.
    이 경우의 문제는 Base의 소멸자가 virtual 이 아니라면 소멸되는 과정에서 상속받은 클래스의 소멸자가 정상적으로 호출되지 않게 되서 정의되지 않는 동작이 일어나게 된다. 즉 기반클래스의 소멸자는 virtual로 선언 되어야 한다. 뒤집어 이야기 한다면 virtual 로 선언되지 않은 소멸자를 같는 클래스는 기반클래스(일반적인 의미의 인터페이스를 제공하는)로 사용되 않을 확률이 높다.

    예를 들어
    class Point{
    public:
    Point(int x, int y);
    ~Point();
    private:
    int x, y;
    };
    은 딱 64비트 크기에 맞는 크기다. 이 경우 64비트 레지스터에 쏙 들어감으로써 최적화된 코드라고 볼수도 있다. 하지만 이 Point 클래스의 소멸자를 virtual 로 선언하는 순간 각 Point 클래스의 객체는 버추얼테이블 포인터를 가져야 하며 64비트 크기를 넘어서게 된다. 즉 이런 경우에는 가상소멸자는 어울리지 않는다.

    다시 한번 강조하면 클래스가 가상함수를 하나 이상 가지고 있는 경우에만 소멸자를 virtual로 선언한다 를 기억해 둬야 한다.

    그런데 많은 실수중에 하나가 C++ , STL 에서 제공하는 표준 클래스들을 상속받는 경우다.
    class GoodString: public std::string{
    };
    // …
    GoodString *pss = new GoodString(“어쩌구저쩌구”);
    std::string* ps = pss;
    delete ps; // GoodString의 소멸자가 호출되지 않음. 에러발생.
    std::string, std::vector 등의 클래스들은 virtual 소멸자를 갖지 않으므로 이들을 상속받아 추가적인 내용을 구현하는 경우 기반클래스의 포인터로 할당하고 해제할 경우 정의되지 않는 동작이 일어나므로 이런식으로 사용해서는 안된다.
    사견으로 위와 같이 기반클래스의 포인터로 메모리 해제만 안하면 별 문제없는것 같다. 즉 처음부터 끝까지 상속받은 클래스의 포인터를 사용하면 된다.
    물론 virtual 소멸자가 아니지만 기반클래스로 제작된 클래스도 있다.
    ex) 부스트의 noncopyable

    const 를 쓰자

    출처 : Effective C++ 3판. 항목 3 : 낌새만 보이면 const를 들이대자.

    const 키워드의 위치에 따른 상수성의 변화
    char greeting[] = “Hello”;

    char *p = greeting; // 비상수 포인터, 비상수 데이터
    const char* p = greeting; // 비상수 포인터, 상수 데이터
    char* const p = greeting; // 상수 포인터, 비상수 데이터
    const char* cosnt p = greeting // 상수 포인터, 상수데이터


    * 에 대한 위치가 중요하다.
    void f1(const Widget *pw);       // f1은 상수 Widget 객체에 대한
    // 포인터를 매개변수로 취합니다.
    void f2(Widget const *pw); // f2도 마찬가지

    Effective C++

    이펙티브 C++ 3판.
    재미있음.

    지하철에서 대충 읽으면서 나중에 시간 나면 자세히 보자고 표시해둔 부분.

    예외처리 (예외에 안전하게 구현하는법)
    항목 25 : 예외를 던지지 않는 swap에 대한 지원도 생각해 보자
    항목 29 : 예외 안전성이 확보되는 그날 위해 싸우고 또 싸우자
    그리고 Eceptional C++ 에서 예외부분을 보면 비슷한 내용이 있음 그것도 참고. 이건 문서화할 필요성이 느껴진다.

    상속시 생기는 심볼(이름)문제
    항목 33 : 상속된 이름을 숨기는 일은 피하자
    상속시 동일한 이름을 같는 함수를 구현했을때 기본클래스의 함수가 가려져서 접근하지 못하게 되는 문제. 이것은 operator new 등과도 연관이 있으므로 메모리관련 재구현시 참고하자.

    private 상속을 관련.
    항목 39 : private 상속은 심사숙고해서 구사하자.
    우선 private 상속은 is-implemented-in-term-of 관계라는 걸 기억해두고. 관심있는 부분은 EBO(공백 기본 클래스 최적화)

    템플릿메타프로그래밍
    항목 47 : 타입에 대한 정보가 필요하다면 특성정보 클래스를 사용하자.
    특성정보를 입력하고(typedef로) 특성정보 클래스를 사용하여 구현하는 오버로딩 기법.
    항목 48 : 템플릿 메타프로그래밍, 하지 않겠는가?
    말 그래도 TMP 에 대한 소개.

    메모리처리 관련
    항목 49 : new 처리자의 동작 원리를 제대로 파악하자.
    항목 50 : new 및 delete를 언제 바꿔야 좋은 소리를 들을지 파악해 두자
    항목 51 : new 및 delete를 작성할 때 따라야 할 기존의 관례를 잘 알아 두자.
    항목 52 : 위치지정 new를 작성한다면 위치지정 delete도 같이 준비하자.

    메모리관련해서 new등을 재구현 할 경우 참고하자.
    사실 클라이언트 코드가 아니라 라이브러리쪽 에서나 위의 new 에 대한 구현 사항을 알아두면 되겠지만… 클라이언트 코드라면 저런 사항 다 지키고 하면서 복잡하게 하는것 보단 그냥 간단하게 자기코드에서만 잘돌아가는걸 만드는것도 방법.

    TR1 이나 Boost 의 사용법을 파악하기
    아놔 왜이리 추가되는게 많아.
    천천히 하자 -_-;

    사용비트사이즈 제한

    회사에서 코드를 보던중

    struct Object
    {
        unsigned value : 16;
    };

    이런 코드가 있길래 뭔가 하고 테스트를 해봤더니 사용하는 비트수를 제한하는 것 같다. (아마도) 테스트를 한 코드는

    #include <iostream>
    using namespace std;

    struct TestStruct
    {
      unsigned value : 16;
    };

    struct TestStruct2
    {
      unsigned value;
    };

    int main()
    {
      TestStruct a;
      TestStruct2 b;

      a.value = 65535;
      b.value = 65535;

      cout<<“sizeof TestStruct : “<<sizeof(TestStruct)<<endl;
      cout<<“value TestStruct::value : “<<a.value<<endl;
     
      a.value++;
      cout<<“value TestStruct::value++ after”<<endl;
      cout<<“value TestStruct::value : “<<a.value<<endl;
     
      cout<<“sizeof TestStruct2 : “<<sizeof(TestStruct2)<<endl;
      cout<<“value TestStruct2::value : “<<b.value<<endl;
     
      b.value++;
      cout<<“value TestStruct2::value++ after”<<endl;
      cout<<“value TestStruct2::value : “<<b.value<<endl;
      return 0;
    }

    이런 느낌!!
    결과는

    sizeof TestStruct : 4
    value TestStruct::value : 65535
    value TestStruct::value++ after
    value TestStruct::value : 0
    sizeof TestStruct2 : 4
    value TestStruct2::value : 65535
    value TestStruct2::value++ after
    value TestStruct2::value : 65536

    음…냐..

    ….
    이런것도 있었구나!!!!

    ps.
    맨 처음에 위에 있는 코드만 보고
    – 아 그냥 원래 데이타타입이랑 똑같은 메모리 공간을 사용하는데 비트수만 제한하나 보군
    이라고 생각했는데

    struct UnionObject
    {
      union
      {
        struct
        {
          unsigned small1 : 16;
          unsigned small2 : 16;
        };
        unsigned int value;
      };
    };

    이 코드를 보니 뭔가 이상하다. 문맥상은 value 와 small1, small2 의 메모리 공간이 공유 되게는걸 의도한것 같은데 위의 결과를 보면 비트수를 제한해도 4바이트를 차지하니 value 와 small1 만 공유될것 같은데… 라고 생각해서 테스트를 해보니 UnionObject는 그냥 4바이트만 차지하는게 아닌가?

    그래서 이것저것 테스트

    struct TestStruct
    {
      unsigned value : 8;
    };

    이 구조체는 4바이트를 차지하는데

    struct TestStruct
    {
      unsigned value : 8;
      unsigned value2 : 8;
      unsigned value3 : 8;
      unsigned value4 : 8;
    };

    이 구조체 역시 4바이트만 차지함. 저기서 하나 더 추가하거나 제한 비트숫자를 1만 올려도 8바이트를 차지한다.

    음… 저게 원래 저런건지 아니면 비주얼스튜디오의 4바이트 정렬 옵션 때문에 저런 결과가 나오는건지는 잘 모르겠다.
    냠… 책에서 읽었던 기억이 살포시 나는데 쓸일이 없다보니 이미 까먹은…. -_-;