정체불명의 모모

Effective STL - Chapter1 효과적인 컨테이너 02 본문

프로그래밍(c++)/Effective STL(C++)

Effective STL - Chapter1 효과적인 컨테이너 02

정체불명의 모모 2021. 8. 4. 19:02

1 - 6 : C++ 컴파일러의 어이없는 분석 결과를 조심하자

상황 : int 데이터가 들어 있는 파일을 list에 복사한다.

ifstream dataFile("ints.dat");

list<int> data(istream_iterator<int>(dataFile),	// 미리 경고입니다! 이 코드는 제대로 작동하지 
		istream_iterator<int>());	// 않습니다.

위 코드의 괄호의 차이점

매개 변수를 둘러싼 괄호는 무시 되어도 무방한 것이지만, 그냥 괄호는 함수의 매개변수 리스트가 있음을 나타내는 것입니다.

즉, 이 그자체가 함수의 포인터임을 알리는 것 입니다.

 

ifstream dataFile("ints.dat");

list<int> data(istream_iterator<int>(dataFile),	// 미리 경고입니다! 이 코드는 제대로 작동하지 
		istream_iterator<int>());	// 않습니다.

위의 문장은 리스트 객체가 아니라 data 라는 이름의 함수를  선언한 것 입니다. (list<int>의 반환 타입을 가진 함수)

 

- 이유 설명 -

1. 첫째 매개 변수는 dataFile이란 이름을 가지고 있습니다.

    타입은 istream_iterator<int> 입니다. dataFile를 둘러싸고 있는 괄호는 불필요한 것이므로  컴파일러가 무시합니다.

 

2. 둘째 매개 변수는 이름을 가지고 있지 않습니다.

    타입은 아무 것도 받아들이지 않고 iostream_iterator<int>를 반환하는 함수 포인터 입니다.

 

▷ 정상적인 초기화

list<int> data((istream_iterator<int>(dataFile)),	// 첫째 매개변수가 괄호로 둘러싸여 있습니다.
	istream_iterator<int>());

위와 같이 하면 data 라는 리스트 객체가 제대로 생성 됩니다.

 

해결책 ↓

▷ 반복자 객체의 이름을 만들어 넣어주기(컴파일러에 구애 받지 않는 방법)

ifstream dataFile("ints.dat");

istream_iterator<int> dataBegin(dataFile);
istream_iterator<int> dataEnd;
list<int> data(dataBegin, dataEnd);

 

1 - 7 : new로 생성한 포인터의 컨테이너를 사용할 때에는 컨테이너가 소멸되기 전에 포인터를 delete 하는 일을 잊지 말자

▷  메모리가 줄줄 새는 코드

void doSomething()
{
	vector<Widget*> vwp;
    
    for(int i = 0; i < SOME_MAGIC_NUMBER; ++i)
    	vwp.push_back(new Widget);
        
      ...
}	// Widget은 여기서 셉니다! 코드가 끝났음에도 메모리 해제를 시켜주지 않았기 때문에

포인터 자체는 vwp가 스코프(scope , 변수의 유효 영역)을 벗어날 때 소멸 되지만, new로 만들어진 객체에 대해 delete가

전혀 쓰이지 않았다는 사실은 바뀌지 않았습니다.

 

▷ 위 코드에 메모리 delete를 해줌

void doSomething()
{
	vector<Widget*> vwp;
    ...
    for(vector<Widget*>::iterator i = vwp.begin(); i != vwp.end(); i++)
    	delete *i;
}

▷ 위 코드를 1. foreach로 변경 2. 예외 처리 해주기

template<typename T>
struct DeleteObject:public unary_function<const T* , void> // 상속 받음 c++ 11 에서 더이상														
{							//사용 안함
	void operator()(const T* ptr) const	// 연산자 재정의를 통해서 구현
    	delete ptr;
}

void doSomething()
{
	...// as before
    for_each(vwp.begin(), vwp.end() , DeleteObject<Widget>);
}

class SpecialString : public string
{	...
};

위 코드는 위험이 많습니다.

 

ㅁ 이유

1. String은 여느 표준 STL컨테이너 처럼 가상 소멸자를 가지고 있지 않아서 ,  가상 소멸자 없이 이것을 public 으로 상속한 클래스는

    아주 큰 문제입니다.

void doSomething()
{
	deque<SpecialString*> dssp;
    ...
    for_each(dssp.begin() , dssp.end(), DeleteObject<string>());
    // 정의 되지 않은 동작이 일어납니다.
    // 가상 소멸자가 없는 기본 클래스의 포인터를 가지고 파생 객체를 삭제 하려고 합니다.
}

문제점 : dssp는 SpecialString* 포인터를 담고 있도록 선언된 deque 인데,  DeleteObject로 String을 Delete 해주고 있다.                해결 방법 : DeleteObject::operator()에 넘겨지는 포인터의 타입을 컴파일러가 직접 판단(deduction)할 수 있도록 해주자.

 

▷ 예외처리 한 코드

// 수정 전
template<typename T>
struct DeleteObject : public unary_fun<const T* , void>
{
    void operator()(const T* ptr) const
    {
    	delete ptr;
     }
};


// 예외 처리 후
struct DeleteObject
{
	template<typename T>
    void operator()(const T* ptr) const
    {
    	delete ptr;
    }
};

수정 전 과 후의 코드를 보면, 수정 전에는 템플릿 대상을 Delete-Object 이지만, 수정 후엔 operator에 템플릿을 받아주고 있습니다.

이제 컴파일러는 DeleteObjecty::operator( ) 에 넘겨지는 포인터의 타입을 알 수 있습니다.

 

▷  예외 처리 후 사용 코드

// 예외 처리 전의 코드
void doSomething()
{
	deque<SpeccialString*> dssp;
    //...
    
	for_each(dssp.begin() , dssp.end() , DeleteObject<string>());
}


// 예외 처리 후의 코드
void doSomething()
{
	deque<SpecialString*> dssp;
    
    ...
    for_each(dssp.begin() , dssp.end(), DeleteObject());
}

하지만 예외 안전성이 없다는 슬픈 현실은 여전히 남아 있습니다.

SpecialString 이 new 된 직후 for_each가 호출되기 전에 예외가 발생하면 누수 테러가 일어날 것 입니다.

 

가장 간단한 방법은 포인터 컨테이너 대신에 스마트 포인터(smart pointer)의 컨테이너를 사용하는것

▷ 스마트 포인터를 이용한 위 코드 구현

void doSomething()
{
	typedef std::shared_ptr<Widget> SPW;
    
    vector<SPW> vwp;
    for(int i = 0 ; i < SOME_MAGIC_NUMBER; ++i)
    	vwp.push_back(SPW(new Widget));
        
    ...
}

메모리 누수를 막기 위해서는 해당 포인터를 참조 카운팅이 가능한 스마트 포인터(shared_ptr 같은)로 대체하든지, 컨테이너가 

    소멸 되기 전에 컨테이너 내의 포인터에 대해 직접 delete를 해주어야 합니다.

 

1 - 8 : auto_ptr의 컨테이너는 절대로 만들지 말자

: auto_ptr의 컨테이너는 절대 금지입니다.

  c++ 11버전 이후엔 사라졌기 때문에, 사용 할 수가 없어 내용에 넣지 않겠습니다.

  현재로는 사용할 수도 없으며, 사용해서는 안되는 스마트 포인터라고 생각하시면 될 것입니다.

 

auto_ptr : auto_ptr은 템플릿 기반이기 때문에 어떤 타입의 포인터든지 받을 수 있습니다.

클래스의 큰 특징은 객체가 지정된 범위(Scope)를 벗어나면 소멸자가 호출 됩니다.

 

  • auto_ptr의 문제점 : new로 생성한 단일 객체에 대해서만 메모리의 해제를 보장한다는 것 입니다.
    malloc 등으로 할당할 경우에는 free가 호출되지 않기 때문에 해제가 안되는것 입니다.
    또한, new[]로 메모리를 할당할 경우 메모리가 정상적으로 해제되지 않을 수 있습니다.
  • 동일한 메모리 위치를 가리키는 객체를 2개 이상 생성하지 않아야 한다는 점입니다.

 

1 - 9 : 데이터를 삭제할 때에도 조심스럽게 선택할 것이 많다.

상황 : int를 담는 표준 STL 컨테이너가 있다.

         그리고 그 컨테이너 안에 있는 정수 중에 1963이라는 값을 가진 것은 모두 지우고 싶다.

 

연속 메모리 컨테이너의 경우를 알아 봅시다.

- 가장 좋은 방법은 erase_remove 합성문 입니다.

c.erase(remove(c.begin(), c.end() , 1963), c.end());
// 컨테이너가 vector, string 혹은 deque 일 때 특정한 값을 가진 요소를 없애는 가장 좋은 방법은
// erase-remove 합성문을 사용하는 것 입니다.

위 방법은 양방향 반복자를 지원하는 list에도 통하지만, list의 멤버 함수인 remove가 더 효율적 입니다. 

 

양방향 반복자를 지원하는 List 의 경우

c.remove(1963);
// 컨테이너가 list일 때에는 remove 멤버 함수가 특정한 값을 가진 요소를
// 모두 없애는 데에 더 좋습니다.

표준 연관 컨테이너일 때에는(set , multiset , map , multimap) remove라는 이름을 가진 어떤 것도 소용이 없습니다.

    연관 컨테이너는 remove 류의 멤버 함수를 가지고 있지 않고 remove 알고리즘을 사용하면 컨테이너값을 덮어써서, 

    컨테이너를 변형시킬  수 있습니다.

 

연관 컨테이너

- 특정한 값을 가진 요소를 지우는 것은 erase 입니다.

c.erase(1963);	
// 컨테이너가 표준 연관 컨테이너일 때에는 erase 멤버 함수가 특정한
// 값을 가진 요소를 모두 없애는 데에 가장 좋습니다.

 이렇게 하면 원하는 삭제가 제대로 될 뿐만 아니라 효율적 입니다.

 로그 시간만큼 만 걸립니다.

비교 데이터 삭제 할때 쓰는 함수
표준 시퀀스 컨테이너
(vector, string ...)
erase_remove
(합성문)
List remove( )
연관 컨테이너 erase( )

 


 

상황 : true를 반환하는 요소를 모두 없애자

  표준 시퀀스 컨테이너

Container<int> c;

// x가 "나쁘다"면 true를 반환 합니다.
bool badValue(int x);

// 컨테이너가 vector, string 혹은 deque일 때 badValue가 true를 반환하는 요소를
// 모두 없애는 가장 좋은 방법 입니다.
c.erase(remove_if(c.begin(), c.end(), badValue), c.end());

// 컨테이너가 list일 때 badValue가 true를 반환하는 요소를 모두 없애는
// 가장 좋은 방법 입니다.
c.remove_if(badValue);

badValue함수에 인자를 넣지 않아도 잘 된다.

  표준 연관 컨테이너 ( 첫 번째 방법 / remove_copy_if )

AssocContainer<int> c;	// 지금, c는 표준 연ㄱ놘 컨테이너 중 하나
...
AssocContainer<int> goodValues;		// 삭제 되지 말아야 할 값을 담아 주기위해 선언.

remove_copy_if(c.begin() , c.end(),	// 삭제 되지 말아야 할 값들을 goodValues에 복사해 줍니다.
			inserter(goodValues,
            		goodValues.end()),
                    badValue);
                   
c.swap(goodValues);	// c 와 goodValues의 내용을 바꿔줍니다.

set을 이용하여 첫번째 방법으로 구현

위의 방법은 삭제되지 않은 요소를 모두 복사 했다가 옮기기 때문에 복사에 걸리는 비용이 우리가 생각한 가격보다 비싸다.

연관 컨테이너는 요소를 처음부터 횡단하는 루프를 넣어, 요소를 직접 검사 해야 합니다.

 

AssocContainer<int> c;
...
for(AssocContainer<int>::iterator i = c.begin(); i != c.end(); ++i)
{
	if(badValue(*i)) 	
    	c.erase(i);
}
// 위 코드는 버그를 품고 있습니다.

(위 코드의 문제는 erase가 수행되고 나서 루프의 ++i 부분에 의해서 i 가 증가하니깐, 무효화된 반복자는 증가시켜 보아야 소용이 없습니다.)

 

▷ 후위 연산자로 수정

AssocContainer<int> c;
...
for(AssocContainer<int>::iterator i = c.begin(); i != c.end();)
{
	if(badValue(*i))
    	c.erase(i++);
    else ++i;
}

후위 연산자를 이용해 데이터 삭제

상황 : 요소가 지워질 때마다 어떤 메시지를 로그 파일에 기록하도록 해보기 

▷ 연관 컨테이너(void)

ofstream logFile;
AssocContainer<int> c;
...
for(AssocContainer<int>::iterator i = c.begin(); i != c.end();)
{
   if(badValues(*i))
    {
    	logFile << "Erasing " << *i <<"\n";
        c.erase(i++);
     }
     else ++i;
 }

▷ 시퀀스 컨테이너 (반환 값을 이용해 요소를 유지 함)

for(SeqContainer<int>::iterator i = c.begin() ; i !=c.end(); )
{
   if(badValue(*i))
    {
    	logFile << "Erasing " << *i << '\n';
        i = c.earse(i);		// erase의 반환값을 i에 저장해서
     }				// 유효성을 유지합니다.
     else ++i;
}

 

erase 함수 반환값을 이용한 삭제

위 코드는 제대로 동작하는 코드이지만, 표준 시퀀스 컨테이너에서만 그렇습니다.

 표준 연관 컨테이너용 erase 함수의 반환값은 void이기 때문에 반환값을 이용할 수 없습니다.

 


◈ 결론 정리

ㅁ 컨테이너에서 특정한 값을 가진 객체를 모두 없애려면 

컨테이너 방       법
vector , string 혹은 deque등 erase-remove 합성문을 사용하자 -> return 
list list::remove( )
표준 연관 컨테이너 erase( ) 함수 -> void

 ㅁ 컨테이너에서 특정한 술어 구문을 만족하는 객체를 모두 없애려면

컨테이너 방       법
vector , string 혹은 deque등 erase-remove_if 합성문을 사용
list list::remove_if( )
표준 연관 컨테이너 remove_copy_if 와 swap 
or
컨테이너 요소 내부를 도는 루프에서 erase를 호출하면서
erase에 넘기는 반복자를 후위 증가 연산자로 증가 시키기

ㅁ 루프 안에서 무엇인가를 하려면(객체 삭제도 포함해서)

컨테이너 방       법
표준 시퀀스 컨테이너 컨테이너 요소를 하나씩 사용하는 루프를 작성합니다.
이때 erase를 호출할 때마다  그 함수의 반환 값으로 반복자를 업데이트 하는 일을 꼭 해야 합니다
표준 연관 컨테이너 컨테이너 요소를 하나씩 사용하는 루프를 작성 합니다.
이때 erase를 호출하면서 erase에 넘기는 반복자를 후위 증가 연산자로 증가시킵니다.

 

1 - 10 : 할당자(allocator)의 일반적인 사항과 제약 사항에 대해 잘 알아두자 (어렵다...)

 

" 대부분의 STL 표준 컨테이너는 절대로 메모리 할당자를 요구하지 않는 지경에 이르렀습니다. "

 

할당자의 제약 사항은 쓸모가 없게 된, 포인터와 참조자에 대한 typedef로 정의된 타입부터 시작합니다.

 

할당자는 본래 메모리 모델에 대한 추상층으로 여겨졌고, 그 자체로서 할당자에서 정의된 메모리의 모델의 포인터와 

참조자에 대한 typedef 타입을 제공하는 것으로 의미가 있었습니다.

C++ 표준안에 의하면, 타입 T의 객체에 대한 디폴트 할당자(allocator<T>)는 아래와 같은 typedef 타입을 제공 하고,

사용자 정의(user-defined) 할당자도 이런 typedef 타입을 제공하도록 정해져 있습니다.

- allocator<T>::pointer 
- allocator<T>::reference

" 할당자는 객체 입니다. "

 

즉 , 할당자는 멤버 함수, 중첩(nested) 타입과 typedef 타입(pointer, reference 등)을 가질 수 있다는 뜻입니다.

 하지만, 표준안에 의하면 같은 타입의 모든 할당자 객체는 동등(equivalent) 하며, 항상 상등 비교를 수행한다(compare equal)는

점을 가정하여 STL 구현을 해야한다고 합니다. 

 

template<typename T>
class SpecialAllocator{...};
typedef SpecialAllocator<Widget> SAW;

list<Widget, SAW> L1;
list<Widget, SAW> L2;
...
L1.splice(L1.begin() , L2);	// L2의 노드를 L1의 앞에다가 옮긴다.

// splice에 의해 어떤 리스트 요소를 다른 리스트 요소에 옮겨 붙일 때에는 복사가 일어나지 않습니다.
// 포인터 몇 개가 바뀌면서 원래의 리스트에 있던 노드가 다른 리스트로 자리를 찾아갈 뿐입니다.
// 이 때문에 splice 동작은 빠른데다가 예외 안전성을 갖추고 있습니다.
질문 : 왜 C++ 표준안에서 STL 제작자는 동일한 타입의 할당자가 동등한 것으로 가정하도록 했는지 아시겠습니까?? 
답 : 어떤 할당자에 의해 할당된 메모리(L2 같은)를 다른 할당자 객체(L1)로 안전하게 해제 할 수 있어야 하기 때문입니다.

이식이 가능한 할당자(객체) : 동등하고, 제대로 동작하는 할당자

 

▷ operator new 와 allocator<T>::allocate

void* operator new(size_t bytes);

pointer allocator<T>::allocate(size_type numObjects);
// "pointer"는 거의 항상 T*에 대응된 typedef 타입 입니다.

위 두가지 타입 모두 할당할 메모리의 양을 매개 변수로 받아들이지만, 

  • operator new → 바이트 수
  • allocator<T>::allocate → 메모리에 맞는 객체의 수

ex) sizeof(int) == 4 인 플랫폼

  • operator new → 4     ( 매개변수 : size_t )
  • allocator<T>::allocate → 1    ( 매개변수 : allocator<T>::size_type )

둘 다 부호 없는 정수값(unsigned int) 입니다.

전형적으로  allocator<T>::size_type은 size_t에 대한 typedef 타입입니다.

operator new 와 allocator<T>::allocate의 매개 변수 규약이 같지 않기 때문에 operator new 를 스스로 만들어 썻을 때의 노하우를

커스텀 할당자에 직접 적용하기가 복잡합니다.

 

▷ 할당자 차이

할당자 반환 타입 설        명 
operator new void* 초기화 되지 않은 메모리의 포인터를 나타낸다.
allocator<T>::allocator T* (pointer라는 typedef 타입)
반환되는 포인터는 T 객체를 가리키지 않습니다.
왜냐 하면 T는 만들어지지 조차 않았기 때문입니다.
실제로 T 객체를 메모리에 만드는 쪽은
allocator<T>::allocate의 호출자(caller)로
정해져 있습니다.
* vector::reserve나 string::reserve의 경우는 제외
차   이 초기화되지 않은 메모리에 대한 개념적 모델이 다름을 알려주는 것 입니다.

 


ㅁ 대부분 표준 컨테이너는 자신이 생성될 때 같이 붙어온 할당자를 한 번도 호출하지 않습니다.

// list<int ,allocator<int>> 와 똑같습니다.
// allocator<int>는 메모리 할당시 전혀 호출 되지 않습니다.
list<int> L;

// SAW는 SpecialAllocator<Widget>의 typedef 타입입니다.
// SAW 역시 메모리 할당에 전혀 호출되지 않습니다.
set<Widget, SAW> S;

list만 그런 것이 아닙니다. STL의 모든 표준 연관 컨테이너(set, multiset , map , multimap)는 모두 이렇게 동작 합니다.

이들은 모두 노드 기반(node-based) 컨테이너라는 공통점을 가지고 있습니다.

즉, 값이 새로 저장될 때마다 새로운 노드가 동적으로 할당 되는 자료구조에 기반을 둡니다.

 list의 경우 각 노드들이 (이중 연결) 리스트의 노드입니다.

표준 연관 컨테이너의 경우 대개 균형 이진 탐색 트리(balance binary search tree)로 구현되어 있기 때문에 각 노드들이 

트리 노드 입니다.

 

▷ list<T> 구현

template<typename T, typename Allocator = allocator<T>>

class list
{
	private:
    	 Allocator alloc;
         struct ListNode
         {
            T data;
            ListNode * prev;
            ListNode * next;
          };
     ...
};

리스트에 새 노드가 하나 추가되면 할당자에서 필요한 메모리를 떼어 와야 합니다.

T를 담고 있는 ListNode 에 대한 메모리 입니다.

template<typename T>
class allocator
{
	public:
    	template<typename U>
        struct rebind
        {
           typedef allocator<U> other;
        };
        
    ...
};
Allocator::rebind<ListNode>::other

List<T>는 Allocator::rebind<ListNode>::other를 참조함으로써 T 객체의 할당자(Allocator)를 통해

ListNode의 할당자를 찾을 수 있습니다.


" 현재.. 할당자에 대해 정확하게 이해가 되지 않아 마소의 도큐멘터리를 찾아봤다. "

 

ㅁ allocator 클래스

: 클래스 템플릿은 형식의 개체 배열에 대한 저장소 할당 및 해제를 관리 하는 개체를 설명 합니다.

  클래스의 개체는  allocator c++ 표준 라이브러리의 여러 컨테이너 클래스 템플릿에 대한 생성자에 지정 된 기본 할당자 개체입니다.

template <class Type>
class allocator

설명

: 모든  c++ 표준 라이브러리 컨테이너에는 기본적으로 설정 되는 템플릿 매개 변수가 allocator 있습니다.

  사용자 지정 할당자로 컨테이너를 생성하면 해당 컨테이너 요소의 할당 및 해제를 제어할 수 있습니다.

 

할당자

: 할당자 개체를 만드는 데 사용되는 생성자 입니다.

allocator();
allocator(const allocator<type>& right);
template <class Other>
	allocator(const allocator<Other>& right);

설명 

: 생성자는 아무 작업도 수행하지 않습니다.

  그러나 일반적으로 다른 할당자 개체로부터 구성된 할당자 개체는 비교 시 같아야 하며,

  두 할당자 개체 간에 개체 할당 및 해제를 서로 혼합할 수 있어야 합니다.

 ( 저자의 말에서 나온것 과 같이 '동등' 하며, 항상 '비교'를 수행 할 수 있어야 한다 했다.)


다시 책의 내용으로 돌아 와서,,,

이 항목은 커스텀 할당자에 관한 내용이다.. 많이 어렵다..

마지막으로 저자의 정리 시간

 

 

  • 할당자를 템플릿으로 만듭니다. 템플릿 매개 변수에는 여러분이 메모리를 할당 하고자 하는 객체의 타입을 나타내는 T를 사용합니다.
  • pointer와 reference 라는 typedef 타입을 제공하되, 항상 pointer는 T*, reference는  T&이 보아도록 합니다.
  • 할당자의 allocate 멤버 함수에는 필요한 객체의 개수를 매개 변수로 넘깁니다.
    바이트 수가 아닙니다. 또한, 이 함수는 T* 포인터(pointer 라는 typedef 타입을 통해) 를 반환(비록 T 객체는 아직 생성 되지 않았
    지만) 한다는 것을 잊지 맙시다.
  • 표준 컨테이너(연관 컨테이너)에서 필요로 하는 rebind라는 중첩 템플릿을 꼭 제공합니다.

 

 

1 - 11 : 커스텀 할당자를 제대로 사용하는 방법을 이해하자

 

1. 공유 메모리 힙으로 관리하자

▷ malloc 과 free 함수를 이용하여 공유 메모리 힙을 관리하는 루틴 

void* mallocShared(size_t bytesNeeded);
void freeShared(void *ptr);

- 위의 공유 메모리 매커니즘을 이용한 STL 컨테이너

template<typename T>
class SharedMemoryAllocator
{
	public:
    	...
        pointer allocate(size_type numObjects , const void *localityHint = 0)
        {   // static_cast<바꾸려고 하는 타입>(대상)
        	return static_cast<pointer>(mallocShared(numObjects * sizeof(T)));
        }
        
        void deallocate(pointer ptrToMemory , size_type numObjects)
        {
        	freeShared(ptrToMemory);
        }
   ...
         
};
// 새로만든 vector
// 실제로 vector의 매개 변수엔 allocator가 들어갑니다.
typedef vector<double, SharedMemoryAllocator<double>> SharedDoubleVec;

{	... 
    SharedDoubleVec v;
}

위의 코드는 거의 절대로 공유 메모리가 아닙니다.

 

▷ 위 코드를 메모리 공유 되게 만들기!

// SharedDoubleVec 객체를 담을 수 있는 충분한 공유 메모리를 할당(allocate) 합니다.
void * pVectorMemory = mallocShared(sizeof(SharedDoubleVec));


// "전용 new(placement new)"를 써서 SharedDoubleVec 객체를 메모리에
// 생성(Construct) 합니다.
SharedDoubleVec * pv = new (pVectorMemory) SharedDoubleVec;

...
// 이 객체를 사용합니다(pv를 통해서) 공유 메모리 내에 있는 이 객체를 소멸(destroy) 시킵니다.
// placement new 를 이용하여 생성한 객체는 delete로 객체를 지우는 게 아니라
// 직접 객체의 소멸자를 불러서 객체를 소멸 시켜줍니다.
pv->~SharedDoubleVec();

// 공유 메모리에 있는 처음 메모리 단위를 해제(deallocate) 합니다.
freeShared(pVectorMemory);

※ 주의_위의 코드는  null 포인터를 반환할 때의 경우는 고려하지 않았습니다.

 

2. 두 개의 힙에서 메모리를 관리하는 것 입니다.

class Heap1
{
	public:
    	...
        static void* alloc(size_t numBytes , const void *memoryBlockToBeNear);
        static void dealloc(void *ptr);
        ...
};

class Heap2 {...};	// Heap1과 동일한 alloc/dealloc 인터페이스를 가지고 있습니다.

▽ 코드 설명 

: Heap1 과 Heap2 등의 클래스를 사용하도록 설계된 할당자를 하나 만들어 실제 메모리 관리를 맡게 합니다.

template<typename T, typename Heap>
SpecialHeapAllocator
{
  public:
    ...
    pointer allocate(size_t numObjects , const void *localityHint = 0)
    {
    	return static_cast<pointer>(Heap::alloc(numObjects * sizeof(T),
               localityHint));
    }
    
    void deallocate(pointer ptrToMemory, size_type numObjects)
    {
    	Heap::dealloc(ptrToMemory);
     }
     ...
};

▽ 코드 설명 

: SpacialHeapAllocator를 이용해 해당 컨테이너의 요소를 한데 모읍시다.

// v와 s의 요소는 Heap1에 모두 모아 놓습니다.
vector<int , SpeciticHeapAllocator<int , Heap1>> v;		 
set<int , SpecificHeapAllocator<int, Heap1>> s;		

// L과 m의 요소는 Heap2에 모두 놓습니다.
list<Widget, SpecificHeapAllocator<Widget, Headp2>> L;
map<int ,string, less<int>, SpecificHeapAllocator<pair<const int, string> , Headp2>> m;

※ 이 예제에서 중요한 것은 Heap1 과 Heap2는 객체가 아니라 타입(type)이라는 점입니다.

    Heap1 과 Heap2가 타입이 아니라 객체(object)인 경우에는 이 둘은 동등한 할당자가 아닙니다.

    즉 이것은 할당자의 동등성(equivalence) 제약을 어기게 되기 때문입니다.

 

 

할당자는 여러 가지 면에서 유용합니다.

"같은 타입의 할당자는 모두 동등해야 한다"는 제약만 잘 지키면,

특수 힙 관리나 공유 메모리 관리도 편리하게 할 수 있습니다.


!!!   여기서 잠깐  !!!

'타입 '과  '객체' 넘나 헷갈립니다.

정리 들어 갑니다!

 

C++ 타입과 객체
  • 타입은 가능한 값과 적용할 수 있는 연산의 집합을 정의 합니다.
  • 객체는 주어진 타입의 값을 저장하는 메모리 공간 입니다.
  • 값은 주어진 타입으로 해석되는 메모리상의 비트의 집합이다.
  • 변수는 명명된 객체다.
  • 선언은 객체의 이름을 지정하는 구문 입니다.
  • 정의는 객체의 메모리 공간을 할당하는 선언의 일종 입니다.

 

1 - 12 : STL 컨테이너의 쓰레드 안전성에 대한 기대는 현실에 맞추어 가지자

STL 컨테이너에 있어서의 다중쓰레딩 지원이라면 SGI(실리콘 그래픽스사)에서 제정한 방식이 가장 권위 있는 표준입니다.

 

▽ 핵심 내용

  • 여러 쓰레드에서 읽는 것은 안전하다(Multiple readers are safe)
    : 하나 이상의 컨테이너의 내용을 동시에 읽어 내는 경우가 있는데, 제대로 동작합니다.
      당연한 이야기이지만 읽기 도중에 쓰기 동작이 수행되면 안됩니다.

  • 여러 쓰레드에서 다른 컨테이너에 쓰는 것은 안전하다(Multiple writers to different containers are safe)
    : 하나 이상의 쓰레드가 다른 컨테이너에 동시에 쓸 수 있습니다.

 

▽  컨테이너의 완벽한 쓰레드 안전성을 구현하려면 라이브러리 쪽에서 어떤 것을 해야 할지 정리

  • 컨테이너의 멤버 함수를 호출하는 시간 동안에 컨테이너에 락(lock)을 걸기
  • 컨테이너가 만들어 내어 주는 반복자의 유효 기간 동안에 컨테이너에 락을 걸기
  • 컨테이너에 대해 실행된 알고리즘의 수행 시간 동안에 컨테이너에 락을 걸기
    (사실 가능하지 않은 이야기 입니다. -> 알아보는 이유는 : 쓰레드 안전성을 이룰 수 없는 이유를 알기 위해서)

- 상황 : vector<int>에서 5란 값이 처음 등장하는 위치를 찾자.(만일 찾았으면 이 값을 0으로 바꿉시다.)

vector<int> v;
...
vector<int>::iterator first5(find(v.begin(), v.end(), 5));	// 첫째 줄
if(first5 != v.end())						// 둘째 줄
{
	*first5 = 0;						//세째 줄
}

위 코드는 무효화 될 수 있는 위험한 코드 입니다.

 

※ 위험한 이유

1. 첫째 줄과 둘째 줄 사이에 다른 쓰레드가 끼어 들어 벡터에 요소를 삽입한다든지 해서 메모리가 재할당되면 first5 반복자가 무효화 되어 버리기 때문입니다.

2. 둘째 줄과 세째 줄 사이에 다른 쓰레드가 끼어 들어 first5가 가르키고 있던 요소를 삭제한다든지 해서 first5를 무효화시킬 수 있습니다.

 

위의 코드가 쓰레드 안전성을 가지려면 v는 첫째 줄부터 세째 줄까지 실행될 동안 계속 락이 걸린 채로 남아 있어야 합니다. - 너무 어려움

 

직접 쓰레드 동기화를 제어해 주는 것이 낫습니다.

 

  Mutex를 이용한 동기화 제어 ( 안전함 )

vector<int> v;
...
getMutexFor(v);
vector<int>::iterator first5(find(v.begin(), v.end() , 5));

if(first5 != v.end())
{
	*first5 = 0 ;
}

releaseMutexFor(x);

 

객체지향적으로 mutex 이용하기

: 생성자에서 mutex를 잡고 소멸자에서 이 뮤텍스를 해제 하는 Lock 클래스 같은 것을 만들어 보자.

 ( getMutexFor의 호출과 release-MutexFor의 호출이 서로 맞지 않을 가능성을 최소화시킬 수 있습니다.)

// 컨테이너에 대한 쓰레드 동기화를 위해 뮤텍스를 얻고, 해제해 주는 클래스의 골격 템플릿
template<typename Container>
class Lock
{
	public:
    	Lock(const Container& container): c(container)
        {
          getMutexFor(c);	// 생성자에서 뮤텍스를 얻어냅니다.
        }
        ~ Lock()
        {
           releaseMutexFor(c);	// 소멸자에서 이 뮤텍스를 해제 합니다.
        }
    private:
    	const Container& c;
};

이렇게 클래스(Lock 등의)를 사용하여 리소스(뮤텍스 같은)의 사용 수명을 관리하는 아이디어는 일반적으로 

"리소스 획득은 초기화이다(resource acquisition is initialization)" 라고 합니다.

 

vector<int> v;
...
{
	Lock<vector<int>> lock(v);	// 뮤텍스를 얻어 냅니다.
    vector<int>::iterator first5(find(v.begin() , v.end() , 5));
    if(first5 != v.end())
    {
    	*first5 = 0;
     }
 }

 참고 사이트 : https://openmynotepad.tistory.com/57

 

STL) 나만의 Allocator( 할당자 ) 만들기 - 1

참고: EASTL, https://jacking75.github.io/Cpp_EASTL/ CUSTOM ALLOCATOR : https://www.youtube.com/watch?v=pP15kDeXJU0 https://www.youtube.com/watch?v=kSWfushlvB8 std 의 container들에는 Allocator(할당..

openmynotepad.tistory.com

https://uncertainty-momo.tistory.com/57

 

[ C++] std::mutex를 통한 thread 동기화

thread가 C++ 11에서 직접 지원하면서 동기화를 위한 std::mutext가 추가되었습니다. mutex : 스레드로 동시에 여러 개를 처리할 때 모든 스레드가 접근하는 데이터를 하나의 스레드가 먼저 사용할 수 있

uncertainty-momo.tistory.com

 

Comments