C++ Plus
Visual C++ 2008 Feature Pack을 사용하여 Windows 응용 프로그램에 새로운 기능 추가
Kenny Kerr
이 기사는 Visual C++ Feature Pack 시험판 버전을 기준으로 합니다. 여기에 포함된 모든 정보는 변경될 수 있습니다.
이 기사에서 다루는 내용:
- Microsoft Foundation Class 업데이트
- C++로 리본 프로그램
- C++로 탭 MDI 구현
- 다형 함수와 스마트 포인터
|
이 기사에서 사용하는 기술:
Visual Studio 2008, MFC
|

목차
Visual C++를 사용하는 개발자라면 최근 몇 년간 Visual C++®보다는 Visual C#®에 새로운 기능이 더 많이 추가되는 모습을 보며 소외감을 느꼈을 것입니다. 사실 Visual C++ 컴파일러는 그동안 성능, 보안 및 표준 준수를 포함한 다양한 영역에서 계속 개선되었지만 새로운 라이브러리 및 생산성 기능은 한동안 거의 추가되지 않았습니다. Windows Vista®를 더 원활하게 지원하도록 MFC가 업데이트되었지만 이것만으로는 부족했습니다.
다행히 Microsoft는 특히 네이티브 코드와 MFC를 사용하는 개발자에 대한 지원을 강화하기 위해 Visual C++ 2008 Feature Pack을 발표했습니다. 여기에서 Visual C++에 대한 새로운 열정을 느낄 수 있습니다.
Feature Pack에는 최신 사용자 인터페이스를 작성하기 위한 새롭고 다양한 MFC 클래스 집합이 포함되어 있습니다. 또한 TR1(Technical Report 1)로 표준 C++ 라이브러리에 추가되고 있는 기능도 다수 포함되어 있습니다. TR1은 C++ 위원회의 승인을 거쳐 표준 C++ 라이브러리에 도입된 첫 번째 주요 업데이트이자 추가 기능입니다.
지난 몇 년 동안 MFC 개발의 주요 요소는 단일 및 다중 문서/뷰 응용 프로그램, 메뉴, 도구 모음 및 대화 상자라는 일반적인 패러다임에 머물렀습니다. MFC 응용 프로그램을 보다 현대적인 느낌으로 만들려는 개발자는 알아서 해야 했습니다.
이제 상황이 바뀌었습니다. MFC에는 이제 Microsoft® Office와 Visual Studio®에서 볼 수 있는 것과 비슷한 도킹 가능 창을 비롯한 새로운 사용자 인터페이스 패러다임이 다양하게 포함되어 있습니다. 또한 Microsoft Office 리본 사용자 인터페이스 및 기타 여러 가지 새로운 컨트롤, 대화 상자와 창에 대한 완전한 지원도 추가되었습니다.
이제부터 MFC의 두 가지 새로운 사용자 인터페이스 기능인 Office 리본과 탭 MDI(다중 문서 인터페이스)에 대해 살펴보겠습니다.
Office 리본 사용자 인터페이스
아마 여러분은 2007 Microsoft Office system의 리본 요소를 본 적이 있을테고, 이러한 요소를 여러분의 응용 프로그램에서 구현할 수 있는지 궁금할 것입니다. 놀랍게도 아주 쉽게 이러한 리본 표시줄을 MFC 프레임 창에 추가할 수 있습니다.
새로운 기능의 대부분은 CWinApp, CFrameWnd 및 CMDIFrameWnd 클래스의 새 버전을 사용하며, 이러한 클래스는 거의 모든 MFC 응용 프로그램에서 기반이 됩니다. CWinAppEx는 CWinApp에서 파생되며 응용 프로그램 개체를 위한 기본 개체로 사용됩니다. CFrameWndEx는 CFrameWnd에서 파생되며 SDI(단일 문서 인터페이스) 프레임 창을 위한 기본 클래스로 사용됩니다. 이와 유사하게 CMDIFrameWndEx는 CMDIFrameWnd에서 파생되며 MDI 프레임 창을 위한 기본 클래스로 사용됩니다. 이러한 새로운 기본 클래스는 도킹 가능한 창과 크기 조정 가능한 창, 그리고 작업 영역 유지와 같은 새로운 사용자 인터페이스의 여러 기능을 지원하는 데 필요한 모든 내부 작업을 처리합니다.
그림 1에서는 리본 표시줄을 지원할 수 있는 최소한의 응용 프로그램 개체를 보여 줍니다. 여기에서 볼 수 있듯이 Application 클래스는 CWinAppEx에서 파생되며 일반적으로 응용 프로그램의 주 창을 만드는 데 사용되는 익숙한 InitInstance 멤버 함수를 구현합니다. 또한 프레임 클래스에서 필요하므로 SetRegistryKey 멤버 함수를 호출하여 응용 프로그램 설정에 대한 레지스트리 위치를 설정해야 합니다. 그러면 InitInstance가 일반적인 절차에 따라 주 창을 만듭니다.

Figure 1 리본 응용 프로그램 개체
class Application : public CWinAppEx
{
public:
virtual BOOL InitInstance();
};
BOOL Application::InitInstance()
{
SetRegistryKey(L"SampleCompany\\SampleProduct");
m_pMainWnd = new MainWindow;
m_pMainWnd->ShowWindow(m_nCmdShow);
m_pMainWnd->UpdateWindow();
return TRUE;
}
그림 2의 코드에서는 리본 표시줄과 응용 프로그램 단추가 있는 최소한의 SDI 프레임 창을 보여 줍니다. 응용 프로그램 단추는 필수적인 것은 아니지만 일반적으로 리본 표시줄과 조합하여 기존 파일 메뉴를 대신하여 응용 프로그램을 위한 일종의 주 메뉴를 제공하는 데 사용됩니다.

Figure 2 리본 프레임 창
class MainWindow : public CFrameWndEx
{
DECLARE_MESSAGE_MAP()
public:
MainWindow();
private:
int OnCreate(CREATESTRUCT* createStruct);
CMFCRibbonBar m_ribbon;
CMFCRibbonApplicationButton m_appButton;
};
BEGIN_MESSAGE_MAP(MainWindow, CFrameWndEx)
ON_WM_CREATE()
END_MESSAGE_MAP()
MainWindow::MainWindow()
{
Create(0, // 클래스 이름
L"MFC Ribbon Sample Application");
}
int MainWindow::OnCreate(CREATESTRUCT* createStruct)
{
if (-1 == __super::OnCreate(createStruct))
{
return -1;
}
if (-1 == m_ribbon.Create(this))
{
return -1;
}
m_appButton.SetImage(IDB_APP_BUTTON);
m_ribbon.SetApplicationButton(&m_appButton,
CSize(45, 45));
CMFCRibbonMainPanel* appButtonMenu =
m_ribbon.AddMainCategory(L"Menu",
IDB_APP_BUTTON_MENU_SMALL,
IDB_APP_BUTTON_MENU_LARGE);
appButtonMenu->Add(new CMFCRibbonButton(ID_FILE_NEW,
L"&New",
0, // 작은 이미지 인덱스
0)); // 큰 이미지 인덱스
appButtonMenu->Add(new CMFCRibbonButton(ID_FILE_OPEN,
L"&Open...",
1, // 작은 이미지 인덱스
1)); // 큰 이미지 인덱스
appButtonMenu->AddToBottom(new CMFCRibbonMainPanelButton(ID_APP_EXIT,
L"E&xit",
15));
//작은 이미지 인덱스
CMFCRibbonCategory* category = m_ribbon.AddCategory(L"Home",
IDB_RIBBON_CAT_HOME_SMALL,
IDB_RIBBON_CAT_HOME_LARGE);
CMFCRibbonPanel* panel = category->AddPanel(L"Clipboard");
panel->Add(new CMFCRibbonButton(ID_EDIT_PASTE,
L"Paste",
0, // 작은 이미지 인덱스
0)); // 큰 이미지 인덱스
panel->Add(new CMFCRibbonButton(ID_EDIT_CUT, L"Cut", 1));
panel->Add(new CMFCRibbonButton(ID_EDIT_COPY, L"Copy", 2));
panel->Add(new CMFCRibbonButton(ID_EDIT_SELECT_ALL,
L"Select All", -1));
m_ribbon.AddCategory(L"Insert",
IDB_RIBBON_CAT_HOME_SMALL,
IDB_RIBBON_CAT_HOME_LARGE);
CMFCVisualManager::SetDefaultManager(
RUNTIME_CLASS(CMFCVisualManagerOffice
2007));
CMFCVisualManagerOffice2007::SetStyle
(CMFCVisualManagerOffice2007::Office
2007_LunaBlue);
return 0;
}
(참고: 프로그래머 주석은 예제 프로그램 파일에는 영문으로 제공되며 기사에는 이해를 돕기 위해 번역문으로 제공됩니다.)
개념상 리본은 각각 패널 그룹을 호스트하는 범주라고 하는 여러 개의 탭으로 구성되어 있습니다. 이러한 패널은 응용 프로그램 고유의 다양한 작업을 나타내는 리본 요소나 컨트롤을 호스트합니다. 리본이 응용 프로그램 단추(왼쪽 위 가장자리의 둥글고 큰 단추)를 호스트하는 경우 사용자가 응용 프로그램 단추를 클릭할 때 표시되는 팝업 창은 리본의 주 범주로 볼 수 있는 패널도 표시합니다.
CMFCRibbonBar 클래스는 리본 표시줄 자체를 구현하며 CMFCRibbonApplicationButton 클래스는 리본 표시줄에서 호스트하고 창 프레임의 왼쪽 위 가장자리에 표시되는 응용 프로그램 단추를 나타냅니다. 리본 표시줄은 일반적으로 WM_CREATE 메시지 처리기 내에서 생성되고 준비됩니다. 리본 표시줄을 만들려면 CMFCRibbonBar의 Create 멤버 함수를 호출하고 연결할 창 프레임의 주소를 제공하기만 하면 됩니다. 그런 다음에는 자유롭게 리본 표시줄을 채울 수 있습니다. AddMainCategory 멤버 함수는 리본에 주 범주를 추가하고 이 패널에 표시될 리본 요소를 추가할 수 있는 CMFCRibbonMainPanel에 대한 포인터를 반환합니다.
AddCategory 멤버 함수를 호출하여 리본의 탭을 나타내는 다른 범주를 추가할 수 있습니다. AddCategory는 CMFCRibbonCategory 개체에 대한 포인터를 반환하며 이 개체의 AddPanel 멤버 함수를 사용하면 여기에 패널을 추가할 수 있습니다. AddPanel은 CMFCRibbonPanel 개체에 대한 포인터를 반환하며 여기에 리본의 주 패널에서와 마찬가지로 리본 요소를 추가할 수 있습니다. 마지막으로 CMFCVisualManager::SetDefaultManager 정적 멤버 함수를 사용하여 프레임 창의 스타일과 모양을 담당하는 비주얼 관리자를 설정할 수 있습니다. 그림 3에서는 리본 표시줄의 단추에 필요한 이벤트 처리기를 추가했다고 가정할 때 리본 응용 프로그램이 어떻게 표시되는지 보여 줍니다.
그림 3 리본 샘플 응용 프로그램
탭 다중 문서 인터페이스
MFC는 오래전부터 문서/뷰 아키텍처를 통해 MDI 구현을 지원했습니다. 그러나 기존 MDI는 그림 4에서 볼 수 있듯이 구식이기 때문에 여러분의 응용 프로그램이 Windows® 95 이후로 업데이트되지 않은 듯한 인상을 줍니다. 현재 대부분의 사용자는 창 가장자리를 따라 표시되는 탭을 통해 여러 문서에 액세스하는 방식을 원하는데, 바로 CMDIFrameWndEx MDI 프레임 창을 통해 이러한 기능을 구현할 수 있습니다.
그림 4 시대에 뒤떨어진 창 (더 크게 보려면 이미지를 클릭하십시오.)
새로운 프레임 창을 지원하도록 여러분의 다중 문서/뷰 응용 프로그램 개체를 업데이트해야 합니다. 그림 5에서는 이를 위한 최소한의 응용 프로그램 개체를 보여 줍니다. 기존 MDI 응용 프로그램 개체와 비슷하지만 몇 가지 부분에 대해서는 설명이 필요합니다.

Figure 5 탭 MDI 응용 프로그램 개체
class Application : public CWinAppEx
{
DECLARE_MESSAGE_MAP()
public:
virtual BOOL InitInstance();
};
BEGIN_MESSAGE_MAP(Application, CWinAppEx)
ON_COMMAND(ID_FILE_NEW, &CWinAppEx::OnFileNew)
END_MESSAGE_MAP()
BOOL Application::InitInstance()
{
SetRegistryKey(L"SampleCompany\\SampleProduct");
VERIFY(InitContextMenuManager());
AddDocTemplate(new CMultiDocTemplate(IDR_CHILDFRAME,
RUNTIME_CLASS(Document),
RUNTIME_CLASS(CMDIChildWndEx),
RUNTIME_CLASS(View)));
MainWindow* mainWindow = new MainWindow();
VERIFY(mainWindow->LoadFrame(IDR_MAINFRAME));
m_pMainWnd = mainWindow;
m_pMainWnd->ShowWindow(m_nCmdShow);
m_pMainWnd->UpdateWindow();
return TRUE;
}
먼저 자식 창 프레임을 위한 런타임 클래스는 기존 CMDIChildWnd 클래스가 아닌 CMDIChildWndEx입니다. 탭 뷰를 전환할 때 사용되는 메뉴 관리자를 준비하기 위해 InitContextMenuManager 함수도 호출해야 합니다.
그림 6은 최소한의 MDI 프레임 창을 보여 줍니다. 이번에도 역시 매우 간단하게 이 새로운 기능을 사용할 수 있습니다. 필요한 작업은 EnableMDITabbedGroups 멤버 함수를 호출하여 MDI 탭 그룹 기능을 활성화하는 것이 전부입니다. CMDITabInfo 클래스는 탭 그룹의 모양과 동작을 사용자 지정하는 데 사용할 수 있는 다양한 멤버 변수를 제공합니다. 그리고 이름에서 암시하듯이 사용자가 다른 뷰를 끌어서 놓으면 세로나 가로로 정렬된 탭 그룹을 만들 수 있도록 합니다. 그림 7에서는 이렇게 만든 탭 그룹의 예를 보여 줍니다.

Figure 6 탭 MDI 프레임 창
class MainWindow : public CMDIFrameWndEx
{
DECLARE_DYNCREATE(MainWindow)
DECLARE_MESSAGE_MAP()
private:
int OnCreate(CREATESTRUCT* createStruct);
};
IMPLEMENT_DYNCREATE(MainWindow, CMDIFrameWndEx)
BEGIN_MESSAGE_MAP(MainWindow, CMDIFrameWndEx)
ON_WM_CREATE()
END_MESSAGE_MAP()
int MainWindow::OnCreate(CREATESTRUCT* createStruct)
{
if (-1 == __super::OnCreate(createStruct))
{
return -1;
}
CMDITabInfo tabInfo;
tabInfo.m_bAutoColor = true;
tabInfo.m_bDocumentMenu = true;
EnableMDITabbedGroups(true, tabInfo);
return 0;
}
그림 7 세련된 탭 MDI 응용 프로그램 (더 크게 보려면 이미지를 클릭하십시오.)
표준 C++ 라이브러리의 새로운 기능
앞에서 언급했듯이 Feature Pack에는 TR1의 일부로 표준 C++ 라이브러리에 대한 여러 추가 기능이 포함되어 있습니다. 여기에는 참조 횟수가 계산되는 스마트 포인터, 다형 함수 래퍼, 해시 테이블 기반 컨테이너, 정규식 등에 대한 지원이 포함됩니다. 다음 시나리오에서 이러한 몇 가지 새로운 TR1 기능에 대해 살펴보겠습니다.
다형 함수 개체
함수를 매개 변수로 전달하거나 나중에 사용하기 위해 저장할 수 있는 값으로 참조하는 기능은 많은 응용 프로그램에 매우 중요합니다. 이 개념은 콜백 함수, 이벤트 처리기 및 비동기 프로그래밍 기능을 비롯한 다양한 일반적인 구조를 구현하는 데 사용할 수 있습니다. 그러나 C++에서 함수는 다루기가 무척 까다로울 수 있습니다. 함수는 주로 C와의 호환성과 높은 성능에 대한 필요성에 의해 설계되었습니다. 이러한 목표는 달성했지만 함수를 저장하거나 전달하거나 궁극적으로는 비동기적으로 호출할 수 있는 개체로 취급하는 데 따르는 어려움에 있어서는 아무 진전이 없었습니다. C++에서 함수와 비슷한 몇 가지 일반적인 구조를 살펴보겠습니다.
가장 먼저 친숙한 비멤버 함수가 있습니다.
int Add(int x, int y)
{
return x + y;
}
이러한 비멤버 함수는 다음과 같이 호출할 수 있습니다.
int result = Add(4, 5);
ASSERT(4 + 5 == result);
함수와 비슷한 두 번째 일반적인 구조는 functor라고 하는 함수 개체입니다.
class AddFunctor
{
public:
int operator()(int x, int y) const
{
return x + y;
}
};
함수 개체는 호출 연산자를 구현하므로 함수처럼 사용할 수 있습니다.
AddFunctor fo;
int result = fo(4, 5);
ASSERT(4 + 5 == result);
다음은 비정적 멤버 함수입니다.
class Adder
{
public:
int Add(int x, int y) const
{
return x + y;
}
};
물론 멤버 함수 호출에는 개체가 필요합니다.
Adder adder;
int result = adder.Add(4, 5);
ASSERT(4 + 5 == result);
지금까지는 좋습니다. 이제 이러한 함수와 비슷한 구조를 나중에 사용하기 위해 저장해야 한다고 가정해 보십시오. 다음과 같이 비멤버 함수에 대한 포인터를 저장할 수 있는 형식을 정의할 수 있습니다.
typedef int (*FunctionPointerType)(int x, int y);
함수 포인터 역시 함수인 것처럼 사용할 수 있습니다.
FunctionPointerType fp = &Add;
int result = fp(4, 5);
ASSERT(4 + 5 == result);
함수 개체 역시 저장할 수 있지만 함수 포인터와 함께 다형적으로 저장할 수는 없습니다.
멤버 함수는 멤버 함수 접근 포인터로 저장할 수 있습니다.
Adder adder;
typedef int (Adder::*MemberFunctionPointerType)(int x, int y);
MemberFunctionPointerType mfp = &Adder::Add;
그러나 멤버 함수 접근 포인터 형식은 비멤버 함수 접근 포인터 형식과 호환되지 않으므로 해당 비멤버 함수와 함께 다형적으로 저장할 수 없습니다. 이것이 가능하다고 하더라도 멤버 함수에는 여전히 멤버 함수 호출의 컨텍스트를 제공하기 위한 개체가 필요합니다.
int result = (adder.*mfp)(4, 5);
ASSERT(4 + 5 == result);
더 설명할 필요 없이 아마 여러분도 개념을 이해했을 것입니다. 다행히 새로운 tr1::function 클래스 템플릿이 해결책을 제공합니다. tr1::function 클래스 템플릿은 템플릿 매개 변수에 정의된 함수 형식에 대한 호출 가능 개체를 보관합니다. 여기에서는 이를 비멤버 함수로 초기화합니다.
function<int (int x, int y)> f = &Add;
int result = f(4, 5);
ASSERT(4 + 5 == result);
함수 개체로도 손쉽게 초기화할 수 있습니다.
function<int (int x, int y)> f = AddFunctor();
새로운 함수 바인딩 기능을 사용하여 이를 멤버 함수로 초기화할 수도 있습니다.
function<int (int x, int y)> f = bind(&Adder::Add, &adder, _1, _2);
bind 함수에 대해서는 조금 뒤에 설명하겠습니다. 여기에서 요점은 이제 비멤버 함수, 함수 개체는 물론 멤버 함수에까지 바인딩할 수 있는 단일 함수 래퍼가 있다는 것입니다. 이 함수 래퍼를 저장하고 나중에 호출할 수 있으며 이러한 모든 작업은 다형적으로 수행됩니다.
또한 함수 래퍼는 다시 바인딩할 수 있으며 일반 함수 포인터와 마찬가지로 nothing으로 설정할 수 있습니다.
function<int (int x, int y)> f;
ASSERT(0 == f);
f = &Add;
ASSERT(0 != f);
f = bind(&Adder::Add, &adder, _1, _2);
bind 함수 템플릿은 표준 C++ 라이브러리의 함수 개체 어댑터(구체적으로 말하자면 std::bind1st() 및 std::bind2nd())를 대신할 수 있는 훨씬 강력한 방법입니다. 이 예에서 bind의 첫 번째 매개 변수는 멤버 함수의 주소입니다. 두 번째 매개 변수는 멤버가 호출될 개체의 주소입니다. 이 예에서 마지막 두 매개 변수는 함수가 호출될 때 확인될 자리 표시자를 정의합니다.
물론 bind는 멤버 함수에만 국한되지 않습니다. 표준 C++ 라이브러리의 multiplies 함수 개체를 바인딩하여 제곱 함수를 만들면 단일 매개 변수를 갖는 함수를 만들 수 있으며 이 함수는 제곱된 매개 변수의 결과를 생성합니다.
function<int (int)> square = bind(multiplies<int>(), _1, _1);
int result = square(3);
ASSERT(9 == result);
tr1::function 클래스 템플릿은 표준 C++ 라이브러리 알고리즘과 문제 없이 작동합니다. 정수 컨테이너가 지정된 경우 다음과 같이 멤버 함수를 사용하여 모든 값의 합계를 계산할 수 있습니다.
function<int (int x, int y)> f = // 초기화
int result = accumulate(numbers.begin(),
numbers.end(),
0, // 초기 값 f);
함수 포인터나 함수 개체를 직접 사용할 경우 가능한 컴파일러 최적화(예: 인라이닝)가 tr1::function 클래스 템플릿 때문에 불가능할 수 있습니다. 따라서 반복적으로 호출되는 누적 알고리즘의 경우처럼 필요한 경우에만 tr1::function 클래스 템플릿을 사용해야 합니다. 가능한 경우 함수 포인터, 멤버 함수 포인터(TR1에서 mem_fn으로 도입됨) 및 함수 개체(예: bind에서 반환된 개체)를 표준 C++ 라이브러리 알고리즘 및 다른 템플릿 기반 알고리즘에 직접 전달해야 합니다.
아직 끝이 아닙니다. 더 흥미로운 문제가 있습니다. 예를 들어 드로잉 화면을 나타내는 Surface 클래스와 이러한 화면에 드로잉을 수행하는 Shape 클래스가 있다고 가정해 보십시오.
class Surface
{
//...
};
class Shape
{
public:
void Draw(Surface& surface) const;
};
이제 컨테이너의 각 모양을 지정된 화면에 그리는 방법을 생각해 보십시오. 다음과 같이 for_each 알고리즘을 사용할 수 있습니다.
Surface surface = // 초기화
for_each(shapes.begin(),
shapes.end(),
bind(&Shape::Draw, _1, surface)); // 잘못됨
여기에서는 bind 함수 템플릿을 활용하여 모양 컨테이너의 각 요소에 대해 멤버 함수를 호출하고 화면을 Draw 멤버 함수에 대한 매개 변수로 바인딩합니다. 그런데 이렇게 하면 Surface가 정의된 방법에 따라 예상대로 작동하지 않거나 컴파일조차 되지 않을 수 있습니다. 문제는 실제로 필요한 것은 참조인데 bind 함수 템플릿은 화면만 복사하려고 시도한다는 점입니다. 다행히 TR1은 참조를 자유롭게 복사할 수 있는 값으로 취급하도록 허용하는 reference_wrapper 클래스 템플릿도 제공합니다. ref와 cref 함수 템플릿은 형식 유추를 통해 reference_wrapper 개체를 만드는 작업을 간소화해 줍니다.
이제 for_each 알고리즘은 reference_wrapper의 도움을 받아 간단하고 효율적으로 모양을 화면에 그릴 수 있게 되었습니다.
for_each(shapes.begin(),
shapes.end(),
bind(&Shape::Draw, _1, ref(surface)));
짐작할 수 있겠지만 새로운 함수 래퍼, 바인딩 기능, 그리고 참조 래퍼를 무수히 많은 방법으로 조합하여 모든 종류의 문제를 매끄럽게 해결할 수 있습니다.
스마트 포인터
스마트 포인터는 C++ 개발자에게 매우 중요한 도구입니다. 필자는 COM 인터페이스 포인터 처리에 ATL의 CComPtr, 그리고 원시 C++ 포인터 처리에 표준 C++ 라이브러리의 auto_ptr을 수시로 사용합니다. 후자는 동적으로 C++ 개체를 만들고 auto_ptr 개체가 범위를 벗어날 때 이 개체가 안전하게 삭제되도록 하려는 경우에 유용합니다.
auto_ptr은 유용하기도 하지만 몇 가지의 시나리오에서만 안전하게 사용할 수 있습니다. 이것은 auto_ptr이 소유권 이전이라는 의미 체계를 구현하기 때문입니다. 즉, auto_ptr 개체를 복사하거나 할당하면 기본 리소스의 소유권이 이전되어 원래 auto_ptr 개체가 소유권을 잃게 됩니다. 이러한 특성은 리소스 할당을 세부적으로 제어할 수 있는 시나리오에서는 확실히 유용하지만 개체를 공유해야 하는 경우도 많으며, 이때 공유 소유권 의미 체계를 구현하는 스마트 포인터가 있다면 매우 유용할 것입니다. 더 중요한 사실은 auto_ptr은 표준 C++ 라이브러리 컨테이너와 함께 사용할 수 없다는 점입니다.
TR1에는 다양한 용도로 사용할 수 있는 두 가지 새로운 스마트 포인터가 도입되었습니다. shared_ptr 클래스 템플릿은 auto_ptr과 거의 비슷하게 작동하지만 리소스의 소유권을 이전하는 대신 리소스에 대한 참조 횟수만 증가시킵니다. 개체에 대한 참조를 보관하는 마지막 shared_ptr 개체가 삭제되거나 재설정되면 리소스는 자동으로 삭제됩니다. weak_ptr 클래스 템플릿은 shared_ptr과 함께 작동하여 호출자가 참조 횟수에 영향을 주지 않고 리소스를 참조할 수 있도록 합니다. 이 기능은 순환 관계가 있는 개체 모델 또는 캐싱 서비스를 구현하려는 경우 유용합니다. 또한 표준 C++ 라이브러리 컨테이너와 함께 사용해도 잘 작동합니다.
비교를 위해 다음과 같은 auto_ptr 사용 예를 살펴보겠습니다.
auto_ptr<int> ap(new int(123));
ASSERT(0 != ap.get());
// ap에서 ap2로 소유권 이전
auto_ptr<int> ap2(ap);
ASSERT(0 != ap2.get());
ASSERT(0 == ap.get());
auto_ptr 복사본 생성자는 소유권을 ap에서 ap2로 이전합니다. shared_ptr의 동작도 마찬가지로 예측할 수 있습니다.
shared_ptr<int> sp(new int(123));
ASSERT(0 != sp);
// 공유 개체의 참조 횟수 증가
shared_ptr<int> sp2(sp);
ASSERT(0 != sp2);
ASSERT(0 != sp);
내부적으로 같은 리소스를 참조하는 모든 shared_ptr 개체는 하나의 제어 블록을 공유하며, 이 제어 블록은 리소스를 함께 소유하는 shared_ptr 개체의 수, 그리고 이를 참조하는 weak_ptr 개체의 수를 추적합니다. weak_ptr 클래스 템플릿을 사용하는 방법은 조금 뒤에 살펴보겠습니다.
shared_ptr도 auto_ptr과 비슷한 멤버 함수를 제공됩니다. 제공되는 멤버 함수에는 역참조, 화살표 연산자, 리소스를 대체하기 위한 reset 멤버 함수, 그리고 리소스의 주소를 반환하는 get 멤버 함수 등이 포함됩니다. 몇 가지의 고유한 멤버 함수도 제공되는데, 여기에는 이름 자체가 unique인 함수도 있습니다. unique 멤버 함수는 shared_ptr 개체가 리소스에 대한 참조를 보관하는 유일한 스마트 포인터인지 여부를 테스트합니다. 다음은 이 작업에 대한 예입니다.
shared_ptr<int> sp(new int(123));
ASSERT(sp.unique());
shared_ptr<int> sp2(sp);
ASSERT(!sp.unique());
ASSERT(!sp2.unique());
use_count 멤버 함수를 사용하면 리소스를 소유하는 shared_ptr 개체의 수도 구할 수 있습니다.
shared_ptr<int> sp;
ASSERT(0 == sp.use_count());
sp.reset(new int(123));
ASSERT(1 == sp.use_count());
shared_ptr<int> sp2(sp);
ASSERT(2 == sp.use_count());
ASSERT(2 == sp2.use_count());
그러나 use_count는 모든 구현에서 항상 일관성 있는 작업을 보장하지는 않기 때문에 디버깅 용도로만 사용을 제한해야 합니다. shared_ptr owns가 무엇인가를 소유하고 있는지 여부를 확인하는 데는 제공되는 unspecified-bool-type 연산자를 사용할 수 있으며 shared_ptr가 고유한 소유자인지 여부를 확인하는 데는 unique 함수를 사용할 수 있습니다.
weak_ptr 클래스 템플릿은 shared_ptr 개체가 소유한 리소스에 대한 약한 참조를 저장합니다. 리소스를 소유한 모든 shared_ptr 개체가 삭제되거나 재설정되면 리소스는 이를 참조하는 weak_ptr 개체가 있는지 여부에 관계없이 삭제됩니다. weak_ptr 개체만으로 리소스를 참조하여 사용하는 것을 방지하기 위해서 weak_ptr 클래스 템플릿은 리소스의 주소를 반환하는 친숙한 get 멤버 함수나 멤버 액세스 연산자를 제공하지 않습니다. 리소스에 액세스하려면 먼저 약한 참조를 강한 참조로 변환해야 합니다. 이 기능은 lock 멤버 함수가 제공합니다(그림 8 참조).

Figure 8 강한 참조로 변환
Surface surface;
shared_ptr<Shape> sp(new Shape);
ASSERT(1 == sp.use_count());
weak_ptr<Shape> wp(sp);
ASSERT(1 == sp.use_count()); // 아직 1임
// 임의의 응용 프로그램 논리...
if (shared_ptr<Shape> sp2 = wp.lock())
{
sp2->Draw(surface);
}
중간에 리소스가 해제되면 weak_ptr 개체의 lock 멤버 함수는 리소스를 소유하지 않은 shared_ptr 개체를 반환합니다. 짐작할 수 있겠지만 shared_ptr과 weak_ptr을 사용하면 여러 응용 프로그램에서 리소스 관리를 크게 간소화할 수 있습니다.
Visual C++ 2008 Feature Pack은 분명히 Visual C++ 라이브러리에 대한 환영할 만한 업그레이드이며 유용하게 사용할 수 있습니다. 여기에서 다루지 못한 부분도 많지만 소개된 내용이 여러분의 흥미를 끌어 직접 살펴볼 수 있는 자극제가 되기를 기대합니다.
Kenny Kerr는 Windows용 소프트웨어를 전문적으로 개발하는 소프트웨어 개발자로, 프로그래밍과 소프트웨어 설계 분야에서 왕성한 집필 및 교육 활동을 하고 있습니다. 문의 사항이 있으면 그의 블로그(
weblogs.asp.net/kennykerr)를 방문하시기 바랍니다.