1-1. What is Concurrency?

C++ Concurrency In Action 2016.05.30 21:19

<C++ cuncurrency in Action Practical Multi-threading>


Chapter 1. Hello! World of Concurrency in C++ ! 

@translater: nolleh

(# 역자주, 의역)



이번 챕터에서는 

* 동시성과 멀티스레딩의 의미

* 왜 동시성과 멀티스레딩을 필요로 할까 

* C++ 동시성 지원의 역사

* C++ 프로그램의 간단한 멀티스레딩


C++ 표준이 공개된 1998년으로부터 13년이 지난 지금, C++ 표준 커미티(# 작성자들)는 언어를 지원하고 그것의 지원라이브러리를 주요한 점검대상(overhaul)으로 제공하고 있습니다. 

새로운 C++ 표준 ( C++ 11 or C++0x ) 은 2011년에 공개되었고 이것의 모든 변화의 묶음(swathe)은 C++ 의 사용을 좀더 쉽고 생산적으로 만들어 주었습니다.


C++ 11 표준에서 가장 중요한 특징은 multi_threded 프로그램에 대한 지원입니다. 

처음으로 C++ 표준이 multi_threded 어플리케이션의 존재를 인정하고 라이브러리의 컴포턴트로 multi_threded 어플리케이션을 쓰기 위한 방법을 제공한 것입니다.

이 지원은 플랫폼에 의존적인 확장기능에 의지하지 않고 multi-thread 프로그램을 작성하는데 도움을 주며 따라서 호환적인 multi_threded 코드를 믿을 수 있는 동작을 보장하며 작성하게 해줍니다.

또한 프로그래머가 어플리케이션의 성능을 향상시키기 위해 일반적인 동시성을,

특히 multi-threaded 프로그래밍에 더욱 더 유의할때 등장하게 됩니다. 

(# 성능향상을 위해 multi-threaded 프로그래밍을 이용할때 사용하게 됩니다.) 


이 책에서는 C++로 다중스레드를 이용한 동시성 프로그램을 다루며 그것들을 가능케 하는 C++언어 특징과 라이브러리 기능들을 다룹니다. 동시성과 멀티스레딩이 무엇을 의미하는지부터 시작하여 당신의 어플리케이션에서 왜 동시성을 추구해야 하는지에 대한 내용부터 다룹니다. 

왜 당신이 동시성을 사용하지 않기를 원할 것인가에 대한 내용을 살펴보고 C++ 의 동시성지원에 대한 overview를 제공하고 C++ 동시성의 간단한 예제로 이번 챕터를 마무리하겠습니다. 이미 멀티스레딩 개발 경험이 있는 독자는 이 섹션을 넘어가도 좋습니다. 다음 챕터들에서는 더 확장된 예제, 라이브러리기능들을 더 깊이 있게 다룹니다. 이 책은 멀티스레딩과 동시성을 위한 C++표준 라이브러리에 대한 레퍼런스(찾아보기)들과 함께 마무리가 됩니다. 

그럼, 동시성과 멀티스레딩이란 어떤걸 의미하는 걸까요 ?



1.1 What is concurrency?

가장 간단하고 기초적인 수준에서 동시성은 두개 이상의 분리된 활동들이 동시에 일어나는 것에 대한 것입니다. 우리는 삶의 일부로 동시성을 마주하고 있죠;우리는 이야기하며 걸을 수 있고, 각각의 손으로 다른 일을 할 수 있으며, 다른사람과 함께,독립적으로 각각 살고 있습니다-내가 수영하는 동안에 당신은 football 게임을 관전할 수 있죠. 그외 등등. 



1.1.1 concurrency in computer systems

컴퓨터의 용어로 동시성이란, 평행하게 하나의 시스템에서 동작하고 있는 다중의 독립된 활동입니다. (순차적으로 하나 수행후 다른 하나를 수행하는게 아니라!) 이건 새로운 현상이 아닙니다.: OS 의 multitasking 은 하나의 컴퓨터에서 다수의 어플리케이션이 동시에 task switching을 통해 동작할 수 있게 하는데, 수년간 흔히 자리잡아온 기능이며 진정한 의미의 동시성을 제공하는 다중 프로세서 high-end 서버머신의 공급은 심지어 더 오래된 이야기입니다. 새로운 것은 복수의 태스크를 그렇게 하는 척이 아닌 정말로 동시에 동작하게 하는 컴퓨터의 보급입니다. 


역사적으로, 대부분의 컴퓨터는 하나의 처리기만을 갖고 있었으며 오늘날의 컴퓨터들도 많은 수가 그렇습니다. 

이런 머신들은 정말로 하나의 테스크만을 한 순간에 수행할 수 있는 대신, 테스크를 1초에 여러번 스위칭 할 수 있습니다. 


한 조각에서는 하나의 테스크를 하고, 그리고 다른 조각에서는 다른 테스크를 처리하고.이 반복은 테스크가 동시에 수행되는 것처럼 보입니다. 이 기법은 task switching 이라 불립니다. 


우리는 이런 시스템에서도 동시성이 있다라고 여전히 얘기할 수 있습니다; 테스크 스위치가 정말 빨라서, 처리기가 다른 것으로 스위치를 할 때 어떤 지점에서 하나의 테스크가 일시 정지한다.라고 말할 수 없기 때문입니다.


테스크 스위칭은 사용자에게도, 어플리케이션 자체에 대해서도 동시성에 대한 착각을 불러일으킵니다. 동시성에대한 환상만이 있기 때문에 단독 처리기의 테스크 스위칭 환경에서 실행하는 어플리케이션의 동작은 진짜 동시성이 제공되는 환경에서의 동작과 갑자기 다를 수 있습니다. 

특히, 메모리 모델에 대한(chapter 5) 잘못된 추측은 이런 환경에서 문제로 대두되지 않을 수 있습니다. 이것은 chapter 10에서 더 깊이 다뤄봅니다.



다수의 처리기를 보유한 컴퓨터는 서버에서 사용되어왔고 고성등의 컴퓨팅 테스크를 처리하기 위해 수년간 사용되어 왔습니다. 그리고 현재 하나의 칩에 하나이상의 코어가 있는 처리기(multicore processors)를 사용하는 컴퓨터는 점차 흔한 데스크탑으로 자리잡고 있습니다. 

다수의 처리기를 보유하든, 다수의 코어를 하나의 프로세서에 보유하든 (혹은 둘다이든), 이 컴퓨터들은 정말로 병렬적으로 task를 처리하기위한 능력을 갖고 있습니다. 우리는 이것을 hardware concurrency라고 일컫습니다.


Figure 1.1 은 두 테스크를 10개의 동일한 사이즈로 조각 내어 동작시키는 컴퓨터의 이상적인 시나리오를 보여주고 있습니다. 듀얼 코어 머신에서는 각각의 테스크가 하나씩 코어를 사용하여 실행될 수 있습니다. 테스크 스위칭을 사용하는 하나의 코어 머신은 각각의 테스크의 조각을 끼워 넣게 됩니다. 

하지만 테스크 조각만이 아니라, 아무것도 하지 않는 공간 역시 추가 되게 됩니다. (다이어그램에서 각 조각사이에서 구분 선이 듀얼 코어의 그것보다 더 두껍게 보여지고 있다.); 이 interleaving(끼워넣기)을 위해서, 하나의 테스크에서 다른 테스크로 전환할 때마다 시스템은 문맥교환을 필요로 하고, 

이것에 시간이 들기 때문입니다. 

문맥 교환을 수행하기 위해서 OS는 CPU 상대의 저장/현재 동작중이던 테스크의 포인터 제공/어떤 테스크로 스위치 될지 확인한 뒤 해당 테스크의 CPU 상태를 reload 합니다.


그 후, 잠재적으로 메모리에 명령(instructions)과 데이터들을 새로운 테스크를 위해 로드하고 CPU 가 명령을 수행하는 데 있어서 지연되는 일이 없도록 캐쉬에 담아야 할 것입니다. (# 메모리에 정보들을 올린뒤 캐쉬에도 올립니다.) 


하드웨어의 동시성이 가능한 프로세서들에서라도 어떤 처리기들은 하나의 코어에서 복수개의 스레드를 실행할 수 있습니다. 이 결정을 위한 중요 고려 요소는 hardware threads의 '수' 입니다 : 얼마나 많은 독립된 테스크를 하드웨어가 정말로 동시성을 보장하며 수행할 수 있는가가 기준인겁니다.


하드웨어 동시성을 보장하는 시스템이라도 병렬적으로 수행 할 수 있는 수 이상으로 테스크가 많을 수 있으며 이때에는 테스크 스위칭을 이용하게 됩니다. 


예를들면, 전형적인 데스크탑 컴퓨터는 평소에는 많은 작업이 없지만 백그라운드 동작을 수행하면서 수백개의 테스크를 동작시킬수 있습니다. 백그라운드 테스크를 동작시키게하는 것이 바로 테스크스위치이며 당신이 워드 프로세서, 컴파일러, 에디터, 웹브라우져를 ( 등등 ) 한번에 이용하게 합니다. 

Figure 1.2 는 4개의 테스크가 듀얼코어 머신에서 이상적인 시나리오로 같은 사이즈 조각으로 나누어 테스크스위칭을 하는 것을 보여주고 있습니다. 실제상황에서는 많은 이슈들이 분할이 동등하게 이뤄지지 않도록 하며 스케쥴링 또한 정규적으로 이뤄지지 않게 합니다. 이런 이슈들의 일부는 Chapter 8에서 동시성 코드의 성능에 영향을 주는 요소를 다룰때 함께 다루고 있습니다.


이 책에서 다뤄지는 모든 테크닉, 함수, 클래스들은 당신의 어플리케이션이  싱글코어 프로세서에서 동작하든, 많은 멀티코어 프로세서에서 동작하든 동시성이 테스크 스위칭을 통해 이뤄지든, 하드웨어 동시성을 통해 이뤄지든 영향받지 않고 이용될 수 있습니다. 

하지만 당신이 상상할수 있듯 당신의 어플리케이션에서 어떻게 동시성을 이용하게 구현할지는 하드웨어 동시성의 여부에 따라 의존될 수도 있습니다. 이런 내용은 chapter 8에서 다루며 C++의 코드에서 동시성을 디자인하는 내용과 함께 다루고 있습니다.

 

1.1.2 Approches to concurrency

프로그래머 한 쌍이 소프트웨어 프로젝트에 함께 일하는 상황을 상상해 봅시다. 만약 당신의 개발자가 다른 사무실에 있다면 둘은 각각의 작업을 서로 방해 받지 않으며 각각 자신의 작업을 레퍼런스 메뉴얼을 작성하며 분리해서 진행할 것입니다. 하지만 커뮤니케이션은 쉽지 않겠죠; 의자를 돌려서 서로 이야기하는 것이 아니라 핸드폰과 이메일로, 사무실로 걸어가서 소통해야 할 테니까요. 또한, 두 사무실이라는 오버헤드, 중복된 레퍼런스 메뉴얼의 카피라는 문제도 있습니다.


이제 당신의 짝이 같은 사무실로 이사를 왔다고 상상해봅시다. 이제 서로 자유롭게 어플리케이션의 디자인에 대해서 토론하며 쉽게 종이와 화이트보드에 디자인 아이디어와 설명을 위해 다이어그램을 그릴수도있습니다. 이제 당신은 하나의 사무실만 관리하면되며 하나의 집합의 리소스들만 있으면 대부분 충분합니다. 부정적인 측면으로는, 당신은 아마도 집중하기 힘들수도 있고 리소스를 공유하는게 문제가 될 수도 있겠습니다. ( 레퍼런스 메뉴얼 어디간거야 ? )


당신의 짝 개발자를 조직하는 이 두가지 방법이 동시성에 대한 두가지 기본적인 접근을 설명합니다. 각각의 개발자는 스레드를 의미하며, 각각의 사무실은 프로세스를 의미합니다. 첫번째 접근은 복수의 싱글스레드 프로세스를 나타내며 이건 각각의 개발자가 하나의 사무실을 보유하는 것과 비슷합니다. 그리고 두번째 접근은 다중스레드 싱글프로세스를 나타내며 이건 두 개발자를 같은 사무실에 두는것과 비슷합니다.


당신은 이 임의의 방식을 결합할수있고 멀티스레디드나 싱글스레디드인 복수 프로세스를 가질수도 있습니다. 하지만 원리는 같습니다. 이제 간략하게 동시성에 대한 두 접근을 어플리케이션에서 보도록 합시다.


Concurrency With Multiple Processes

하나의 어플리케이션에서 동시성을 만드는 첫번째 방법은 어플리케이션을 다수의 분리된 싱글스레디드 프로세스들로 나누는 것이며 이 프로세스들은 동시에 동작하여 당신이 웹브라우져와 워드 프로세서를 동시에 사용하는 것과 같습니다. 

이 분리된 프로세스들은 메시지를 서로 normal interprocess communication channels ( signals, sockets, files, pipes, and so on ) 을 이용하여 Figure 1.3 처럼 교환할 수 있습니다. 

이런 프로세스간의 통신의 하나의 단점은 운영체제가 다른 프로세스의 데이터를 실수로 수정하는 일이 없도록 프로세스간의 보안시스템이 구축되어있기 때문에 종종 구축에 복잡할 수 있고 느릴수도 있다는 것입니다. 다른 단점은, 다중 프로세스 구동에서의 상속된 오버헤드 입니다.: 프로세스를 실행하기 위해서는 운영체제가 관리에 필요한 리소스를 할당하는 등의 시간이 소요됩니다. 


물론, 단점만 있는 것은 아닙니다: 운영체제의 추가된 보안은 일반적으로 프로세스와 고수준의 커뮤니케이션 메커니즘사이에 더 쉽게 안전한 동시성 코드를 제공합니다. (스레드 보다) 

정말로, Erlang 프로그래밍 언어에서 제공되는 환경은 좋은 효과를 내기 위해 프로세스를 동시성의 근본 구축 블록으로 사용합니다.


Concurrency with multiple threads

다른 동시성에 대한 접근은 다중 스레드를 하나의 프로세스에서 구동시키는 겁니다. 스레드들은 가벼운 프로세스들과 비슷한 개념입니다: 각각의 스레드는 다른 스레드와 독립적으로 구동되며 다른 명령들의 시퀀스로 동작합니다. 

하지만 모든 스레드들은 프로세스에서 같은 주소공간을 공유하며 대부분의 데이터는 모든 스레드에서 직접적으로 접근될수 있습니다- 전역 변수는 전역적으로 남아있으며 객체에대한 포인터나 레퍼런드, 데이터는 스레드 사이에서 전달될 수 있게 됩니다. 


종종 프로세스들 사이에서 메모리를 공유하는 것은 가능하지만 같은 데이터에 대한 메모리 주소가 다른 프로세스에서는 필요치 않기 때문에 이건 종종 복잡하고 관리하기 어려울 수도 있습니다. Figure 1.4 에서는 하나의 프로세스에 존재하는 두 스레드가 공유된 메모리를 통하여 통신하는 모습을 보여주고 있습니다.


공유된 주소공간과 데이터 보호의 부재는 멀티플 스레드를 사용하는 데 필요한 오버헤드를 다중 프로세스를 사용하는 데 필요한 오버헤드와 비교했을때 운영체제가 기록할(bookkeeping) 필요가 더 적기 때문에 상당히 작습니다. 

하지만 공유된 메모리의 유연성은 비용으로 다가 올 수도 있습니다. 만약 데이터가 다중 스레드에서 접근이 될때, 어플리케이션 프로그래머가 보여지는 데이터의 뷰를 각각의 스레드가 접근할 때마다 일관성을 유지해야하기 때문입니다. 이 스레드간 데이터 공유를 둘러싼 이슈와 피하기 위한 가이드라인들은 이 책의 3,4,5,8 chapter 에서 다루고 있습니다. 

이 문제는 코드를 작성할때의 적절한 관심으로 극복할 수 있으며, 여기서 말한 적절한 관심이, 그렇다고해서 엄청난 비용을 스레드 간에 통신을 위해 필요로 한다는 것을 의미하는 것은 아닙니다.


론칭과 통신에 관계된 상대적으로 멀티프로세스에 비해 적은 오버헤드는 잠재적인 공유 메모리 문제에도 불구하고 C++ 언어를 포함하여 선호되는 접근방법이라는 것을 의미합니다. 


더하여, C++ 표준은 프로세스간의 통신을 위한 어떤 내장된 지원도 하고 있지 않으므로 다중 프로세스를 이용하는 어플리케이션은 결국 플랫폼에 특화된 API 들에 의존해야 합니다. 

이 책은 멀티스레딩만을 배제적으로 다룰 것이며 앞으로의 동시성에 대한 레퍼런스들 또한 다중 스레드를 통해 성취될 수 있다고 가정합니다.

동시성에 대한 의미를 갖고 이제 왜 당신의 어플리케이션에서 동시성을 사용해야 하는지 살펴봅시다.

저작자 표시 비영리 변경 금지
신고

'C++ Concurrency In Action' 카테고리의 다른 글

1-1. What is Concurrency?  (2) 2016.05.30

Chapter 4 - 1. Containers and Algorithms, Libraries

Chapter 4. Containers and Algoritms

4.1 Libraries 

프로그래밍 언어에는 라이브러리가 필요하다. 

C++ 의 STL 기능들을 살펴볼 것이다. 


4.1.1 Standard-Library Overview

제공되는 라이브러리는 아래와 같이 분류됨.


* 런타임 언어 지원 ( 할당과 런타임 타입 정보 )

* C standard library ( 타입시스템에 위배하는 것에 대한 최소한의 수정 )

* 문자열과 I/O 스트림 (국제적 문자셋과 지역화 지원을 포함)

* 컨테이너의 프레임 워크 / 알고리즘 

* 숫자 계산

* 정규식 표현

* 동시성 프로그래밍을 위한 지원

* 템플릿 메타 프로그래밍을 위한 유틸들 ( type traits ), 제네릭 프로그래밍, 제너럴 프로그래밍

* 스마트 포인터 / 가비지 콜렉터

* 특별한 목적의 컨테이너 ( array / bitset / tuple )


라이브러리를 써야하는 이유

* 프로그래머의 수준에 상관없이 유용하다

* 오버헤드 없이 일반적인 형태 제공

* 배우기 쉽고 간단한 사용


4.1.2 The Standard-library Headers and Namespace 

헤더 포함으로 이용가능, std:: prefix


4.2 Strings

문자열 리터럴에서 제공할 수 있는 정보를 보충하기 위한 string 타입을 제공함.

문자열붙이기 (concatenation) 같은 문자열 연산에 용이.

string 은 move 생성자를 가지고 있으므로 긴 문자열이라도 효율적으로 반환 될 수 있음.


String 은 수정가능하므로 = , += , [], substring 연산들이 지원된다.

또 비교 연산도 가능.


4.3 Stream I/O

포맷팅된 캐릭터 인풋과 아웃풋을 iostream 을 통해 지원한다.


4.3.1 Output

모든 빌트인 타입에 대해 출력을 정의해 놓았음. 또 사용자 정의 타입에 대해서도 아웃풋을 정의하기 용이하다.

<< ("put to") 연산자는 ostream 타입의 객체에 대해 사용되며, 출력을 위해 사용된다.

cerr 는 에러 리포팅을, cout 은 표준 출력을 의미.

cout 으로 쓰여진 값은 기본적으로 문자열의 시퀀스로 변환 된다.

즉 cout << 10; 에서 1다음에 0이 있는 문자열의 시퀀스로 노출되게 된다.


캐릭터 상수는 따옴표로 나타내지며 캐릭터는 숫자가 아닌 문자로 취급된다.

int b = 'b'; 

char c = 'c';

cout << 'a' << b << c 

일때 'b' 는 98이므로 다음과 같이 출력됨 a98c


4.3.2 Input

istream 을 입력을 위해 제공. 반대로 >> ("get from") 연산을 포함

공백은 입력을 종료시키므로 라인을 얻기위해서는 getline() 와 같은 별도함수 필요.

개행문자는 버려진다.


확장을 위한 정립된 방식이 존재하기 때문에 먼저 최대 사이즈를 계산할 필요가 없다.


4.3.3 I/O of User-Defined Types

ostream& operator<<(ostream& os, const Entry& e)

{

return os << e.name << e.number ; 

}

와 같이 사용자 정의 타입도 사용가능


비슷하게


istream& operator>>(istream& is, Entry& e)

{

char c,c2;

if (is>>c && c == '{' && is>>c2 )

return is;

else{

is.setf(ios_base::failbit);

return is;

}

}


와 같이 정의된 형식의 입력인지 확인하는 용도로 사용될 수 있다.

is >> c 는 "is 로부터 읽어 c 로 넣는데 성공했는가?" 의 의미를 가진다.

is.get(c) 는 공백을 그냥 넘어가지 않는다. ( 입력한다 ) 



저작자 표시 비영리 변경 금지
신고

Chapter 3 - 1. Abstraction Mechanisms, Classes

Chap 3. A tour of C++: Abstraction Mechanisms ( 추상화 메커니즘 )


3.2 Classes


잘 선택된 클래스의 집합으로 구성된 프로그램은 이해가 쉽고 빌트인 타입만을 사용하여 구성된 것보다 더 올바른 구성이다.

3가지 중요 클래스

* Concrete classes

* Abstract classes

* Classes in class hierarchies

더 많은 클래스가 이 클래스를 조합하여 구현될 수도 있다.


3.2.1 Concrete Types

빌트인 타입과 비슷하게 동작한다. 이 타입의 정의의 특징은 그것의 표현이 정의의 일부에 있다는 것에 있다.

이 타입은 다음과 같은 동작이 가능하다.  

* 구체화 타입의 객체는 스택에 위치한다.

* 객체를 직접 참조한다. ( 포인터나 레퍼런스를 통하지 않고 ) 

* 객체를 즉시, 완벽하게 초기화 하는 것 가능

* 객체를 복사하는 것이 가능


private 로 representation 을 위치 시킬수도 있지만 어쨌든 존재하는 것이므로 변경된다면 재컴파일이 필요.

유연성을 향상시키기위해 representation 의 주요 부분을 free store 에 두고 클래스 객체 그 자체에 저장된 부분을 통해 접근하게 하는 것도 방법. vector / string 은 그렇게 구현됨. 


3.2.1.1 An Arithmetic Type

class complex {

double re, im; // representation: two doubles

public:

complex(double r, double i) :re{r}, im{i} {} // construct complex from two scalars

complex(double r) :re{r}, im{0} {} // construct complex from one scalar

complex() :re{0}, im{0} {} // default complex: {0,0}

double real() const { return re; }

void real(double d) { re=d; }

double imag() const { return im; }

void imag(double d) { im=d; }

complex& operator+=(complex z) { re+=z.re , im+=z.im; return ∗this; } // add to re and im

// and return the result

complex& operator−=(complex z) { re−=z.re , im−=z.im; return ∗this; }

complex& operator∗=(complex); // defined out-of-class somewhere

complex& operator/=(complex); // defined out-of-class somewhere

};


클래스 정의 그자체는 representation 에 접근하기 위한 연산을 포함하고 있다.

클래스에 정의한 함수들은 기본적으로 인라인화 되며, 이것은 함수콜을 통하지 않게 됨을 의미, 빠른 연산을 가능하게 할 수 있다.


 인자 없는 생성자를 default constructor 라고 하며 이것은 초기화 되지 않은 객체가 존재하지 않음을 보장해 준다.

함수 옆에 const 키워드는 이 함수가 이 객체에 대해 변경을 하지 않을 것임을 의미한다.


많은 연산자들은 complex 에 직접적인 표현에 대한 접근을 요하지 않으므로 클래스 정의로부터 분리하여 정의 할 수 있다.


complex operator+(complex a, complex b) { return a+=b; }

complex operator−(complex a, complex b) { return a−=b; }

complex operator−(complex a) { return {−a.real(), −a.imag()}; } // unar y minus

complex operator∗(complex a, complex b) { return a∗=b; }

complex operator/(complex a, complex b) { return a/=b; }


이 연산자를 overloaded operators ( 연산자 오버로드 ) 라고 함. 

단항 연산자는 오버로드가 불가능하며 기본 연산자 의미에 맞지않는 연산자로 오버로드하는 것은 불가능하다.


3.2.1.2 A Container

컨테이너는 요소들의 집합을 포함한 것들을 뜻한다. 따라서 Vector 도 훌륭한 컨테이너.

하지만 우리가 정의한 vector 는 리소스를 반환하지 않는 것에서 큰 문제가 있는데, 

C++ 에 가비지 컬렉터의 의미가 있지만 어떤 환경에서는 이용하지 못할 수도 있고, 제공하는 것보다 더 정밀한 소멸이 필요할 수도 있다.


이때 사용할 수 있는 메커니즘이 소멸자이다.

다음과 같이 delete 키워드를 붙여 사용. 


˜Vector() { delete[] elem; } // destructor: release resources


벡터는 빌트인 타입과 같은 룰로 명명되고, 영역이 정의되며, 할당되며, 라이프타임등이 관리된다.

constructor 에서 리소스를 얻고 destructor 에서 리소스를 반환하는것은 Resource Acquisition Is Initialization / RAII 로 알려져 있으며, 

에러를 만들지 않는 습관이므로, 중요하다


3.2.1.3 Initializing Container

벡터는 요소를 컨테이너에 넣는 편리한 방법이 필요하다.

두가지 방법을 선호한다.

* initializer-list constructor : 요소의 리스트로 초기화

* push_back() : 새 엘리먼트를 시퀀스의 종단에 추가


다음과 같이 선언할수 있다. 


class Vector {

public:

Vector(std::initializ er_list<double>); // initialize with a list

// ...

void push_back(double); // add element at end increasing the size by one

// ...

};


push_back 은 임의의 개수의 입력을 넣는데 좋다.


Vector read(istream& is)

{

Vector v;

for (double d; is>>d;) // read floating-point values into d

v.push_back(d); // add d to v

return v;

}


이때 is 는 eof 를 만나거나 포멧팅에러를 만나면 종료된다.


{1,2,3,4} 와 같이 리스트를 전달받았을때 STL 에 정의된 std::initializer_list 타입으로 컴파일러가 생성하여 프로그램에 전달해주게 되며,

따라서 위와 같은 생성자가 호출될 수 있다.


3.2.2 Abstract Types

추상화 타입은 유저를 자세한 구현으로부터 자유롭게 한다. 

인터페이스를 표현으로부터 분리하고 지역변수를 포기하는 식으로 얻어질 수 있다. 

표현에 대한 정보가 없기 때문에 힙영역에 할당 되어야 하며, 따라서 레퍼런스와 포인터로 취급된다. 


벡터의 추상화 타입 container 를 정의하였다.

class Container {

public:

virtual double& operator[](int) = 0; // pure virtual function

virtual int size() const = 0; // const member function (§3.2.1.1)

virtual ˜Container() {} // destructor (§3.2.1.2)

};

virtual 키워드는 이 추상타입으로부터 파생된 클래스가 다시 정의할 수도 있음을 의미한다.

= 0 는 pure virtual function 임을 의미하는 것이며 반드시 정의해야함을 의미한다.


이 추상화 타입은 다음과 같이 이용 될 수 있다.

void use(Container& c)

{

const int sz = c.size();

for (int i=0; i!=sz; ++i)

cout << c[i] << '\n';

}


size() 와 [] 는 어떠한 구현에 대한 정보도 갖지 않았지만 이용될 수 있다. 이런 다른 클래스로의 다양함의 인터페이스를 제공하는 클래스를 polymorphic type 이라 칭한다.

어떤 표현을 초기화해야할지 알 수 없기 때문에 constructor 가 존재하지 않으며, 반대로 소멸자는 추상화 타입이 포인터나 레퍼런스로 취급되기때문에 컨테이너를 파괴하기 위해 정의 되어 있다.

 

이 추상화 타입을 부모로 하여 vector_container 를 선언 할 수 있다.


class Vector_container : public Container { // Vector_container implements Container

Vector v;

public:

Vector_container(int s) : v(s) { } // Vector of s elements

˜Vector_container() {}

double& operator[](int i) { return v[i]; }

int size() const { return v.siz e(); }

};


[] 와 size() 는 오버라이드 되어있으며 소멸자 또한 부모 소멸자를 오버라이드 했다. 

자식 소멸자는 부모로부터 묵시적으로 호출된다.


비슷하게 List_container 도 선언 할 수 있으며 ,

g() 함수는 Vector_contanier 를 사용하게, h() 함수는 List_container 를 사용하게 구현해 보았다.


3.2.3 Virtual Functions

이제 다시 use() 함수를 고려해보면, 어떤 [] 를 호출해야할지 어떻게 알 수 있을까?

g() 함수에서는 vector_container의 [] 를 호출해야하고, h() 함수에서는 list_container의 [] 를 호출해야 하지만 

use() 함수에서는 vector 인지 list 인지 정확한 정보를 알 수 없다.


비밀은 vtbl ( virtual function table ) 이다. 

이 테이블에서는 가상함수의 포인터를 인덱싱하고 있으며, 따라서 container 의 인덱스와 vector / list 의 인덱스를 매칭시켜 적합한 함수가 호출되게 된다. (부모 vtbl 의 인덱스를 확인, 자식 vtbl에 매칭되는 함수를 호출한다 )


3.2.4 Class Hierachies

이전의 컨테이너로부터 우리는 간단한 클래스 계층구조를 살펴봤다.

클래스 계층구조란, 계층적인 관계를 나타내는 개념이라고 보면 된다.

원과 삼각형 모두 모양이라는 큰 개념의 한 종류라고 보는 식이다.




계층구조를 이용, 정의.....위와 별 다를게 없음, 중략... 

추상클래스에서는 가상 소멸자를 정의하는게 중요한데, 자식클래스의 객체는 보통 추상부모 클래스로부터 제공된 인터페이스를 통해 조작되기 때문이다. 특히, 부모의 포인터를 통해 삭제될 수 있다. 따라서 가상 함수 콜 메커니즘이 적절한 소멸이 이뤄지도록 보장해준다.


클래스 계층구조는 2가지 장점이 있다.

* 인터페이스 상속 : 부모 클래스의 객체가 필요한 어떤 곳에서도 자식 클래스의 객체가 이용될 수 있다. 

* 구현 상속 : 자식클래스의 구현을 간단히 한 함수/ 데이터를 제공하는 것도 가능하다.


다음과 같이 사용했다고 하자.


enum class Kind { circle, triangle , smiley };

Shape∗ read_shape(istream& is) // read shape descriptions from input stream is

{

// ... read shape header from is and find its Kind k ...

switch (k) {

case Kind::circle:

// read circle data {Point,int} into p and r

return new Circle{p,r};

case Kind::triangle:

// read triangle data {Point,Point,Point} into p1, p2, and p3

return new Triangle{p1,p2,p3};

case Kind::smiley:

// read smiley data {Point,int,Shape,Shape,Shape} into p, r, e1 ,e2, and m

Smiley∗ ps = new Smiley{p,r};

ps−>add_eye(e1);

ps−>add_eye(e2);

ps−>set_mouth(m);

return ps;

}

}


void user()

{

std::vector<Shape∗> v;

while (cin)

v.push_back(read_shape(cin));

draw_all(v); //call draw() for each element

rotate_all(v,45); //call rotate(45) for each element

for (auto p : v) delete p; // remember to delete elements

}


user() 는 어떤 shape 에 대해 조작하고 있는지 어떠한 아이디어도 없다.

한번 컴파일 되고 나면 새로운 shape 가 추가되더라도 새로 컴파일 하지 않아도 정상적으로 동작하게 된다.

user() 외에는 shape 에 대한 포인터가 없기 때문에 여기서 반환을 진행한다. 

경험있는 프로그래머는 이 코드의 문제를 눈치챘을 것이다.

* user 는 read_shape() 로부터 반환된 포인터를 delete 하는데 실패할 수 있다.

* shape 포인터의 컨테이너의 소유주는 가리키고 있는 포인터를 delete 하지 않을 수 있다.

이런 점에서, 힙영역에 할당된 객체의 포인터를 반환하는 것은 위험하다.


이때 사용할 수 있는 해결책 중에 한가지는 unique_ptr 를 이용하는 것이다.


case Kind::circle:

// read circle data {Point,int} into p and r

return unique_ptr<Shape>{new Circle{p,r}}; // §5.2.1

// ...


void user()

{

vector<unique_ptr<Shape>> v;

while (cin)

v.push_back(read_shape(cin));

draw_all(v); // call draw() for each element

rotate_all(v,45); //call rotate(45) for each element

} // all Shapes implicitly destroyed


unique_ptr 을 소유하고 있는 객체는 필요하지 않을 때(즉, unique_ptr 의 적용범위가 종료될때) delete 를 호출할 것이다.



저작자 표시 비영리 변경 금지
신고


티스토리 툴바