할당자
"오늘의AI위키"의 AI를 통해 더욱 풍부하고 폭넓은 지식 경험을 누리세요.
1. 개요
할당자는 C++에서 메모리 할당을 관리하는 데 사용되는 클래스이다. 1994년 C++ 표준 라이브러리(STL)에 도입되었으며, STL 컨테이너가 메모리 모델에 독립적이 되도록 하는 데 기여했다. 할당자는 메모리 할당 및 해제, 객체 생성 및 파괴를 담당하며, 사용자 정의를 통해 성능 향상이나 디버깅을 위한 기능을 구현할 수 있다. C++11 표준에서 할당자 인터페이스가 개선되어 상태를 가진 할당자의 유용성이 증가했다.
더 읽어볼만한 페이지
- C++ 표준 라이브러리 - 표준 템플릿 라이브러리
표준 템플릿 라이브러리(STL)는 알렉산더 스테파노프의 주도로 탄생한 C++ 라이브러리로서, 제네릭 프로그래밍을 지원하기 위해 컨테이너, 반복자, 알고리즘, 함수 객체 등의 핵심 구성 요소로 이루어져 있으며, HP에서 무료로 공개한 구현을 기반으로 다양한 구현체가 존재한다. - C++ 표준 라이브러리 - Iostream
`iostream`은 C++에서 입출력 작업을 수행하기 위한 표준 라이브러리로, 스트림 버퍼, 지원 클래스, 입/출력 스트림으로 구성되며, 표준 입력, 출력, 에러, 로깅 객체를 제공하고 출력 형식 제어 기능을 제공하지만 비판도 존재한다. - 제네릭 프로그래밍 - 다형성 (컴퓨터 과학)
다형성은 프로그래밍 언어에서 이름이 여러 자료형에 사용되거나 객체가 여러 타입으로 취급되는 능력으로, 코드 재사용성과 유연성을 높이며 임시 다형성, 매개변수 다형성, 서브타이핑 등으로 분류되고 객체 지향 프로그래밍의 중요한 특징이며 정적, 동적 다형성으로 구현된다. - 제네릭 프로그래밍 - 표준 템플릿 라이브러리
표준 템플릿 라이브러리(STL)는 알렉산더 스테파노프의 주도로 탄생한 C++ 라이브러리로서, 제네릭 프로그래밍을 지원하기 위해 컨테이너, 반복자, 알고리즘, 함수 객체 등의 핵심 구성 요소로 이루어져 있으며, HP에서 무료로 공개한 구현을 기반으로 다양한 구현체가 존재한다.
| 할당자 |
|---|
2. 배경
알렉산더 스테파노프와 멍 리는 1994년 3월 C++ 표준 위원회에 표준 템플릿 라이브러리(STL)를 제시했다. 이 라이브러리는 몇 가지 문제점이 제기되었음에도 불구하고 예비 승인을 받았다. 특히 스테파노프는 라이브러리 컨테이너가 기본 메모리 모델에 독립적이어야 한다는 요구를 받았고, 이것이 할당자 개념의 도입으로 이어졌다. 결과적으로 모든 STL 컨테이너 인터페이스는 할당자를 수용하도록 다시 작성되어야 했다.
'''할당자 요구 사항'''을 충족하는 모든 클래스는 할당자로 사용될 수 있다. 특히, `T` 형식의 객체에 대한 메모리를 할당할 수 있는 `A` 클래스는 다음의 요구 사항을 만족해야 한다.
STL을 C++ 표준 라이브러리에 포함시키는 과정에서 스테파노프는 앤드루 코니그와 비야네 스트롭스트룹을 포함한 표준 위원회 여러 위원들과 긴밀히 협력했다. 이들은 사용자 정의 할당자가 잠재적으로 영속성 저장소 STL 컨테이너를 구현하는 데 사용될 수 있다는 점을 발견했는데, 스테파노프는 당시 이를 "중요하고 흥미로운 통찰력"이라고 평가했다. 스테파노프는 할당자의 이점에 대해 "이식성 관점에서 볼 때, 주소, 포인터 등과 관련된 모든 머신별 요소는 작고 잘 이해된 메커니즘 내에 캡슐화되어 있습니다."라고 설명했다.
그러나 원래의 할당자 제안은 위원회에서 아직 승인되지 않은 일부 언어 기능, 즉 템플릿 자체를 템플릿 인수로 사용하는 기능을 포함하고 있었다. 스테파노프에 따르면, 이러한 기능은 당시 컴파일러로 컴파일할 수 없었기 때문에 "비야네[스트롭스트룹]와 앤디[코니그]의 시간을 들여 이러한 구현되지 않은 기능을 제대로 사용하고 있는지 확인해야 하는 엄청난 요구"가 있었다. 라이브러리가 이전에는 포인터 및 참조 유형을 직접 사용했지만, 할당자 도입 이후에는 할당자에 의해 정의된 유형만을 참조하게 되었다. 스테파노프는 나중에 "STL의 멋진 특징은 머신 관련 유형을 언급하는 유일한 장소가 (...) 대략 16줄의 코드 내에 캡슐화되어 있다는 것"이라고 설명하며 할당자의 역할을 강조했다.
스테파노프는 원래 할당자가 메모리 모델을 완전히 캡슐화하도록 의도했지만, 표준 위원회는 이러한 접근 방식이 용납할 수 없는 효율성 저하를 초래할 수 있다고 판단했다. 이를 해결하기 위해 할당자 요구 사항에 추가적인 제약 조건이 포함되었다. 특히 컨테이너 구현은 포인터 및 관련 정수형에 대한 할당자의 형 정의(typedef)가 기본 할당자가 제공하는 것과 동일하다고 가정할 수 있게 되었고, 주어진 할당자 유형의 모든 인스턴스는 항상 동일하게 비교된다고 가정하게 되었다. 이는 할당자에 대한 원래 설계 목표와 실질적으로 모순되었으며, 상태를 가지는 할당자의 유용성을 크게 제한했다.
스테파노프는 나중에 할당자가 "이론적으로는 나쁘지 않은 아이디어지만 (...) 불행하게도 실제로 작동하지 않는다"고 회고했다. 그는 할당자를 실제로 유용하게 만들려면 참조와 관련하여 핵심 언어의 변경이 필요하다고 지적했다.
2011년 C++ 표준 개정에서는 주어진 유형의 할당자가 항상 동일하게 비교되고 일반 포인터를 사용해야 한다는 제약 조건을 제거했다. 이러한 변경으로 상태를 가진 할당자가 훨씬 더 유용해졌으며, 할당자가 프로세스 외부의 공유 메모리를 관리하는 것도 가능해졌다. 현재 할당자의 주요 목적은 기본 하드웨어의 주소 모델을 추상화하는 것이 아니라, 프로그래머에게 컨테이너 내의 메모리 할당 및 해제 방식을 제어할 수 있는 유연성을 제공하는 것이다. 실제로 개정된 표준은 할당자가 C++ 주소 모델에 대한 확장을 나타낼 수 있다는 원래의 개념을 공식적으로 제거했다.
3. 요구 사항
=== 형식 정의 ===
할당자 클래스 `A`는 다음과 같은 형식들을 정의해야 한다. 이는 `T` 형식의 객체 및 객체에 대한 참조(또는 포인터)를 일반화하여 선언하기 위함이다.
표준 라이브러리 구현은 할당자의 `A::pointer`와 `A::const_pointer`가 단순히 `T*`와 `T const*`에 대한 `typedef`라고 가정할 수 있지만, 라이브러리 구현자는 더 일반적인 할당자를 지원하는 것이 권장된다.
=== 멤버 함수 ===
할당자 클래스 `A`는 다음과 같은 멤버 함수들을 제공해야 한다.
: `n`개의 `T` 형식 객체를 담을 수 있을 만큼 충분히 큰, 새로 할당된 메모리 블록의 시작 주소를 반환한다. 이 함수는 메모리만 할당하며, 객체를 생성하지는 않는다. 선택적 인자 `hint`는 `A`에 의해 이미 할당된 객체를 가리키는 포인터로, 새로운 메모리가 할당될 위치에 대한 힌트를 제공하여 지역성을 향상시키는 데 사용될 수 있다. 그러나 구현은 이 힌트를 무시할 수 있다.
: 이전에 `allocate` 호출을 통해 얻은 포인터 `p`가 가리키는 메모리 블록을 해제한다. `n`은 해제할 요소의 개수이며, `allocate` 호출 시 전달했던 값과 동일해야 한다. 이 함수는 메모리만 해제하며, 객체를 파괴하지 않는다.
: `allocate` 함수 호출로 성공적으로 할당할 수 있는 `T` 형식 객체의 최대 개수를 반환한다. 이 값은 일반적으로 `A::size_type(-1) / sizeof(T)`이다.
: 참조 `x`가 가리키는 객체의 실제 주소를 `A::pointer` 타입으로 반환한다.
: 상수 참조 `x`가 가리키는 객체의 실제 주소를 `A::const_pointer` 타입으로 반환한다.
=== 객체 생성 및 파괴 (C++17 사용 중단, C++20 제거됨) ===
메모리 할당/해제와 객체 생성/파괴는 별도로 처리된다. 과거 표준에서는 할당자가 다음 두 멤버 함수를 제공해야 했다. 이 함수들은 C++17에서 사용이 중단되었고 C++20에서 제거되었다.
: 포인터 `p`가 가리키는 위치에 `val` 값을 사용하여 `T` 타입의 객체를 생성한다.
: 포인터 `p`가 가리키는 위치의 객체를 파괴한다.
이 함수들의 일반적인 구현은 다음과 같다.
: `template
: `void A::construct(A::pointer p, A::const_reference t) {`
: ` new ((void*) p) T(t); // 배치 new 사용`
: `}`
:
: `template
: `void A::destroy(A::pointer p){`
: ` ((T*)p)->~T(); // 소멸자 직접 호출`
: `}`
위 코드는 배치 `new` 구문을 사용하여 지정된 메모리 위치에 객체를 생성하고, 소멸자를 직접 호출하여 객체를 파괴한다.
=== 기타 요구 사항 ===
: `template
: `struct A::rebind {`
: ` typedef A other;`
: `};`
예를 들어, `int` 타입 객체에 대한 `IntAllocator` 할당자 타입이 주어졌을 때, `long` 타입 객체에 대한 관련 할당자 타입은 `IntAllocator::rebind
4. 사용자 정의 할당자
사용자 지정 할당자를 작성하는 주된 이유 중 하나는 성능 개선이다. 특별히 제작된 사용자 지정 할당자를 사용하면 프로그램의 성능이나 메모리 사용량, 혹은 두 가지 모두를 상당히 향상시킬 수 있다. C++의 기본 할당자는 보통 `operator new`를 사용하여 메모리를 할당하는데,[1] 이는 종종 C 언어의 힙 할당 함수를 간단히 감싸서 구현된다. 이러한 기본 할당자는 일반적으로 크고 드물게 메모리 블록을 할당하는 데 최적화되어 있어, vector나 deque처럼 큰 메모리 덩어리를 할당하는 컨테이너에는 잘 작동할 수 있다.
그러나 map이나 list와 같이 작은 객체를 자주 할당해야 하는 컨테이너의 경우, 기본 할당자를 사용하는 것은 일반적으로 느리다. malloc 기반 할당자의 다른 일반적인 문제점으로는 낮은 참조 지역성과 과도한 메모리 조각화가 있다.
성능을 개선하기 위해 널리 사용되는 방법 중 하나는 메모리 풀 기반 할당자를 만드는 것이다. 이 방식은 컨테이너에 항목을 추가하거나 제거할 때마다 메모리를 할당하고 해제하는 대신, 프로그램 시작 시점에 미리 큰 메모리 블록(메모리 풀)을 할당해 둔다. 사용자 지정 할당자는 개별 할당 요청이 있을 때 이 풀에서 메모리 주소를 반환한다. 메모리 해제는 실제로 메모리 풀의 수명이 끝날 때까지 미룰 수 있어 할당 및 해제에 드는 비용을 줄일 수 있다. Boost C++ Libraries에서 메모리 풀 기반 할당자의 예를 찾아볼 수 있다.
사용자 지정 할당자의 또 다른 유용한 용도는 메모리 관련 오류 디버깅이다. 디버깅 정보를 저장하기 위해 추가적인 메모리를 할당하는 할당자를 작성하여 이를 수행할 수 있다. 이러한 디버깅용 할당자는 특정 메모리 블록이 동일한 유형의 할당자에 의해 할당되고 해제되었는지 확인하는 데 사용될 수 있으며, 버퍼 오버런에 대한 제한적인 보호 기능도 제공할 수 있다.
5. C++11의 할당자 개선 사항
C++11 표준은 "범위 지정" 할당자를 허용하도록 할당자 인터페이스를 개선하여, 문자열 벡터나 사용자 정의 타입의 집합 목록 맵과 같이 "중첩된" 메모리 할당을 가진 컨테이너가 모든 메모리가 컨테이너의 할당자에서 비롯되도록 보장할 수 있도록 했다.
6. 예제 코드
다음은 GCC의 확장 기능인 `__gnu_cxx::new_allocator`를 사용하여 메모리를 할당하는 간단한 C++ 예제 코드이다. 이 코드는 `RequiredAllocation`이라는 클래스를 정의하고, `new_allocator`를 통해 해당 클래스 객체를 위한 메모리를 할당하고 해제하는 과정을 보여준다. 특히, 메모리 할당 요청이 실패했을 때 `std::bad_alloc` 예외가 발생하는 상황과 이를 처리하는 방법을 보여준다.
#include
// __gnu_cxx 네임스페이스에 있는 new_allocator를 사용하기 위해 필요할 수 있음
// (표준 헤더에 포함된 경우도 있음)
#include
#include
// 편의를 위해 네임스페이스 사용 선언
using namespace std;
using namespace __gnu_cxx;
// 메모리 할당이 필요한 예시 클래스
class RequiredAllocation
{
public:
RequiredAllocation ();
~RequiredAllocation ();
// 간단한 문자열 멤버 변수
std::string s = "hello world!\n";
};
// 생성자: 객체 생성 시 메시지 출력
RequiredAllocation::RequiredAllocation ()
{
cout << "RequiredAllocation::RequiredAllocation()" << endl;
}
// 소멸자: 객체 소멸 시 메시지 출력
RequiredAllocation::~RequiredAllocation ()
{
cout << "RequiredAllocation::~RequiredAllocation()" << endl;
}
// 할당자(all)를 사용하여 주어진 크기(size)만큼 메모리를 할당하는 함수
// pt는 할당된 메모리 주소를 가리키는 포인터 (여기서는 사용되지 않음)
// t는 RequiredAllocation 객체 포인터 (멤버 접근 예시용)
void alloc(__gnu_cxx::new_allocator
try
{
// size개의 RequiredAllocation 객체를 저장할 메모리 할당 시도
// pt는 할당 힌트로 사용될 수 있으나, new_allocator는 이를 무시함
RequiredAllocation* allocated_ptr = all->allocate(size, pt);
cout << "Successfully allocated memory for " << size << " objects." << endl;
// 할당자가 할당할 수 있는 최대 객체 수 출력
cout << "Max size: " << all->max_size() << endl;
// 할당된 메모리에 객체를 생성하거나 사용하는 대신,
// 예시로 전달된 객체 t의 멤버 변수 s를 출력
cout << "Accessing member of passed object t: ";
for (auto& e : t->s)
{
cout << e;
}
// 할당된 메모리 해제 (실제 사용 시 필요)
// 이 예제에서는 할당 성공/실패만 보여주므로 해제 코드는 생략됨
// all->deallocate(allocated_ptr, size);
}
catch (const std::bad_alloc& e) // 메모리 할당 실패 시 예외 처리
{
// 표준 라이브러리 bad_alloc 예외 발생
cout << "Allocation failed: " << e.what() << endl;
}
}
int main ()
{
// RequiredAllocation 타입에 대한 new_allocator 인스턴스 생성
__gnu_cxx::new_allocator
new __gnu_cxx::new_allocator
// RequiredAllocation 객체 생성
RequiredAllocation t;
// 객체의 주소를 void 포인터로 저장 (alloc 함수 인자로 전달)
void* pt = &t;
/**
// 매우 큰 크기(시스템 메모리를 초과할 가능성이 높은 크기)로 할당 시도
// size_t는 일반적으로 unsigned int보다 크므로 size_t 사용 권장
size_t large_size = 1073741824; // 약 1GB에 해당하는 객체 수 (매우 큼)
cout << "Attempting to allocate memory for " << large_size << " objects..." << endl;
alloc(all, large_size, pt, &t); // pt 대신 nullptr 전달 가능
cout << "\n"; // 출력 가독성을 위한 줄바꿈
// 작은 크기(일반적으로 성공 가능성이 높은 크기)로 할당 시도
size_t small_size = 1;
cout << "Attempting to allocate memory for " << small_size << " object..." << endl;
alloc(all, small_size, pt, &t); // pt 대신 nullptr 전달 가능
// 할당자 메모리 해제
delete all;
return 0;
}
- `#include
`: GCC의 `new_allocator`를 사용하기 위해 필요한 헤더 파일이다. (표준 헤더 ` ` 등에 포함될 수도 있다.) - `RequiredAllocation` 클래스: 메모리 할당의 대상이 되는 간단한 클래스 예시이다. 생성자와 소멸자에서 메시지를 출력하여 객체의 생성 및 소멸 시점을 보여준다.
- `alloc` 함수:
- `__gnu_cxx::new_allocator
* all`: `RequiredAllocation` 객체를 위한 `new_allocator` 포인터. - `size_t size`: 할당할 객체의 개수.
- `void* pt`: 할당 힌트로 사용될 수 있는 포인터. `new_allocator`는 이 인자를 무시한다.
- `RequiredAllocation* t`: 멤버 접근 예시를 위해 전달된 객체 포인터.
- `all->allocate(size, pt)`: `size`개의 `RequiredAllocation` 객체를 저장할 수 있는 메모리 청크를 할당한다. 성공하면 할당된 메모리의 시작 주소를 반환하고, 실패하면 `std::bad_alloc` 예외를 발생시킨다.
- `try...catch(const std::bad_alloc& e)`: `allocate` 호출 중 발생할 수 있는 `std::bad_alloc` 예외를 잡아 처리한다. 메모리 부족 등으로 할당에 실패하면 이 블록이 실행된다.
- `all->max_size()`: 할당자가 이론적으로 할당할 수 있는 최대 객체 수를 반환한다. 이는 사용 가능한 메모리 크기와 객체 크기에 따라 달라진다.
- `main` 함수:
- `new_allocator` 인스턴스를 동적으로 생성한다.
- `RequiredAllocation` 객체 `t`를 생성한다.
- `alloc` 함수를 두 번 호출한다:
1. 매우 큰 `size` (1073741824)로 호출하여 메모리 할당 실패와 `std::bad_alloc` 예외 발생을 유도한다.
2. 작은 `size` (1)로 호출하여 메모리 할당 성공을 보여준다.
- 사용한 `new_allocator` 인스턴스의 메모리를 해제한다 (`delete all`).
이 예제는 할당자의 기본적인 사용법, 특히 메모리 할당 요청과 실패 시의 예외 처리 메커니즘을 보여주는 데 중점을 둔다. 실제 애플리케이션에서는 할당된 메모리에 객체를 생성(`construct`)하고 사용 후에는 객체를 파괴(`destroy`)한 뒤 메모리를 해제(`deallocate`)하는 과정이 필요하다.
참조
[1]
간행물
"[[ISO/IEC 14882]]:2003(E): Programming Languages – C++ § 20.4.1.1 allocator members [lib.allocator.members]'' para. 3"
"[[International Organization for Standardization|ISO]]/[[International Electrotechnical Commission|IEC]]"
2003
[2]
웹사이트
__gnu_cxx::new_allocator< typename > Class Template Reference
https://gcc.gnu.org/[...]
본 사이트는 AI가 위키백과와 뉴스 기사,정부 간행물,학술 논문등을 바탕으로 정보를 가공하여 제공하는 백과사전형 서비스입니다.
모든 문서는 AI에 의해 자동 생성되며, CC BY-SA 4.0 라이선스에 따라 이용할 수 있습니다.
하지만, 위키백과나 뉴스 기사 자체에 오류, 부정확한 정보, 또는 가짜 뉴스가 포함될 수 있으며, AI는 이러한 내용을 완벽하게 걸러내지 못할 수 있습니다.
따라서 제공되는 정보에 일부 오류나 편향이 있을 수 있으므로, 중요한 정보는 반드시 다른 출처를 통해 교차 검증하시기 바랍니다.
문의하기 : help@durumis.com