음악, 삶, 개발
19. Vector, Templates, and Exceptions 본문
19. Vector, Templates, and Exceptions
Lee_____ 2020. 7. 28. 00:28Intro
이 chapter 에서는 아래의 사항들을 배울것이다.
-
vector (STL container)
-
element 의 갯수가 바뀌는 container 를 구현하는 방법
-
element type 을 container 의 parameter 로 사용하는 방법
-
range error 를 대체하는 법
-
design
-
template
-
exception
-
resource management
19.1 The problems
우리는 chapter 18 에서 아래의 사항들을 배웠다.
-
우리가 원하는 element 갯수만큼 vector 만들기.
-
대입 (assignment) 과 초기화 (initialization) 를 통해 vector 를 copy 하기.
-
scope 를 사용하여 vector 가 사용하는 memory 를 release 하기.
- vector 의 element 를 subscript [ ] 를 사용하여 접근하기
이미 vector 는 충분히 유용하다고 chapter 18 에서 배웠지만,
여전히 아래와같은 의문점은 남아있다.
-
어떻게 vector 의 size 를 변경하는지?
-
어떻게 vector 의 out-of-range 접근을 잡아낼수있는지?
-
어떻게 vector 의 element type 을 함수 argument 로 만들수있는지?
19.2 Changing size
vector 를 resizing 하는것은 매우 간단하다.
vector<double> v(n); // v.size() == n
v.resize(10); // v now has 10 elements.
v.push_back(7); // v now has 11 elements.
v = v2; // assign another vector; v is now a copy of v2
// now v.size() == v2.size()
std::vector 는 erase(), insert() 같은 함수또한 제공한다. (B.4.7)
19.2.1 Representation
우리는 대부분 vector 의 push_back() 같은 함수를 매우 자주 호출할것이다.
따라서, 우리는 우리의 프로그램의 vector 의 size 가 매우 자주 바뀔것이라고 생각하며, 최적화(optimize)를 할수있다.
vector 는 사실 free space 의 공간과, element 의 갯수를 항상 추적하여 미래의 확정을 대비한다.
예를 들어.
class vector {
private :
int sz; // number of elements
double* elem; // address of first element
int space; // number of elements plus "free space / slots" for new elements (the current allocation)
};
위의 코드를 그림으로 표현하면 아래와 같다.
vector 가 처음 생성되었을때에는 free space 가 없는 space == sz 상태이다.
우리가 push_back() 함수를 호출하기전까지 (element 갯수를 변화하기전까지)
추가적인 slot 을 allocate 하지않는다. (no memory overhead)
empty vector 를 생성할시 default constructor 는 size 와 space 를 0 으로 설정하고,
elem 은 nullptr 이 된다.
vector::vector() : sz{0}, elem{nullptr}, space{0} { }
19.2.2 reserve and capacity
우리가 vector 의 size (element 의 갯수) 를 변경하고자할때
사용하는 함수가 vector::reserve() 이다.
직접 구현한다면 아래와 같다.
void vector::reserve(int newalloc) {
if (newalloc <= space) {
return; // never decrease allocation
}
double* p = new double[newalloc]; // allocate new space
for (int i = 0; i < sz; ++i) {
p[i] = elem[i]; // copy old elements
}
delete[] elem; // deallocate old space
elem = p;
space = newalloc;
}
이 함수는 새 element 를 위해 space 를 추가한다.
우리는 vector::capacity() 라는 함수를 통해 free space 가 얼마나 있는지 알수있다.
int vector::capacity() const { return space; }
따라서, capacity() - size() 를 하면 우리가 push_back() 할수있는 element 의 갯수를 알수있다.
19.2.3 resize
우리가 resize() 라는 함수를 직접 만든다고 가정해본다면,
아래의 case들을 다룰수있어야한다.
-
new size 는 old allocation 보다 크다.
-
new size 는 old size 보다 크지만, old allocation 보다는 작거나 같다.
-
new size 는 old size 와 같다.
-
new size 는 old size 보다 작다.
아래와 같이 code 를 짜볼수있다.
void vector::resize(int newSize) {
reserve(newSize);
for (int i = sz; i < newSize; ++i) {
elem[i] = 0;
sz = newSize;
}
}
19.2.4 push_back
우리가 push_back() 을 직접 만든다고 가정해본다면,
아래와 같이 구현할수있다.
// increase vector size by one; initialize the new element with d
void vector::push_back(double d) {
if (space === 0) {
reserve(8); // start with space for 8 elements
}
else if (sz == space) {
reserve(2 * space); // ge more space
}
elem[sz] = d; // add d at end
++sz; // increase the size (sz is the number of elements)
}
우리가 남는 space 가 더 이상 없다면, 우리는 할당된 size 를 double 한다.
실제 std::vector 도 위와 같은 전략을 구현되어있다.
19.2.5 Assignment
2개의 서로 size 가 다른 vector 인 v1 과 v2 가 있다고 가정해보자.
우리는 v1 에 v2 를 복사하고싶다.
v1 = v2;
우리가 직접 이 assignment 을 구현한다면 조건은 아래와 같다.
-
copy 를 위한 memory 를 할당한다.
-
copy 한다.
-
old allocation 을 delete 한다.
-
sz, elem, space 를 새 value 로 set 한다.
아래는 위의 조건에 따라 작성한 코드이다.
vector& vector::operator = (const vector& a) {
if (this == & a) return *this; // self-assignment, no work needed.
if (a.sz <= space) {
// enough space, no need for new allocation.
for (int i = 0; i < a.sz; ++i) elem[i] = a.elem[i]; // copy elements.
sz = a.sz;
return *this
}
double* p = new double[a.sz]; // allocate new space.
for (int i = 0; i < a.sz; ++i) p[i] = a.elemt[i]; // copy elements
delete [] elem; // deallocate old space
space = sz = a.sz; // set new size
elem = p; // set new elements
return *this;
}
관습적으로, assignment 연산자 ( = ) 는 reference 를 return 하는것이 일반적이다.
*this 에 대해서는 17.1 을 참고하라.
19.2.6 Our vector so far
우리가 만든 double 을 위한 vector 이다.
class vector {
public :
vector() : sz{0}, elem{nullptr}, space{0} {}
explicit vector(int s) : sz{s}, elem{new double[s]}, space{s} {
for (int i = 0; i < sz; ++i) elem[i] = 0;
}
vector(const vector&); // copy constructor
vector& operator = (const vector&); // copy assignment
vector(vector&&); // move constructor
vector& operator = (vector&&); // move assignment
~vector() { delete[] elem ;} // destructor
double& operator [] (int n) { return elem[n]; } // access : return reference
const double& operator[](int n) const { return elem[n]; }
int size() const { return sz;}
int capacity() const { return space; }
void resize(int newsize);
void push_back(double d);
void reserve(int newalloc);
private :
int sz;
double* elem;
int space;
};
constructor, default constructor, copy operations, destructor 같은 필수적인 연산을 가지고있다.
data 를 accessing 하기위한 subscript [] 를 가지고있으며,
size() 와 capacity() 를 통해 data 의 정보를 제공하고,
resize(), push_back(), reserve() 를 통해 growth 를 컨트롤한다.
19.3 Templates
우리는 double 뿐만 아니라 다른 type 도 vector 에서 자유롭게 사용하고싶다.
예를 들어,
vector<double>
vector<int>
vector<Month>
vector<Window*>
vector<vector<Record>> // vector of vectors
vector<char>
위와 같이 하기위해서는, Template 을 배워야한다!
std::vector 또한 template 이다.
stand libray 는 우리가 필요한 대부분의 것들을 제공하지만,
알아서 잘 작동한다는 이 magic 을 그저 믿기만해서는 안된다.
어떻게 standard library 가 구현되었는지 공부하는것은 매우 중요하다.
왜냐면, stand library 를 만들기위해 사용된 기술들은 우리가 code 를 짤때도 매우 유용한것들이기때문이다.
chapter 21 과 22 에서 standard library 가 어떻게 template 을 사용해 구현되었는지
본격적으로 다룰것이다.
template 의 뜻은 아래와 같다.
template : type 을 class 나 function 의 parameter (매개변수) 로 사용하게해주는 메카니즘.
19.3.1 Types as template parameters
type 을 parameter 로 만드는 C++ 의 기보법 (notation) 은 아래와 같다.
template<typename T>
template 을 활용해 vector 의 code 를 아래와 같이 구현해볼수있다.
template<typename T>
class vector {
public :
vector() : sz{0}, elem{nullptr}, space{0} {}
explicit vector(int s) : sz{ s }, elem{ new T[s] }, space{ s } {
for (int i = 0; i < sz; ++i) elem[i] = 0;
}
vector(const vector&); // copy constructor
vector& operator = (const vector&); // copy assignment
vector(vector&&); // move constructor
vector& operator = (vector&&); // move assignment
~vector() { delete[] elem; } // destructor
T& operator[](int n) { return elem[n]; }
int size() const { return sz; }
int capacity() const { return space; }
void resize(int newsize);
void push_back(const T& d);
void reserve(int newalloc);
private :
int sz;
T* elem;
int space;
};
위와 같은 template 을 만들었다면, 이제 아래와같은 code 를 사용할수있다.
vector<double> vd; // T is double
vector<int> vi; // T is int
vector<double*> vpd; // T is double*
vector<vector<int>> vvi; // T is vector<int>, in which T is int
compiler 가 만약 vector<char> 라는 code 를 본다면,
char type 으로된 vector class 를 어딘가에 (somewhere) 생성한다.
class 의 이름또한 compiler 가 내부적으로 정한다.
/* vector<char> 가 사용되었음 */
/* compiler 는 어딘가에 이 class 를 생성함 */
class vector_char {
int sz;
char* elem;
int space;
// bla bla
}
class template 를 type generator 라고 부르기도한다.
class template 으로부터 실제 class 가 생성되는 과정을 specialization 또는 template instantiation 이라고 한다.
template instantiation 은 굉장히 복잡한 작업이지만, 이 복잡함은 user 에게는 해당되지않고,
compiler 에게 해당된다.
따라서 template instantiation 은 run-time 이 아닌 compile-time 또는 link-time 에 수행된다.
template 은 함수에 사용될수있다.
template<typename T> void vector<T>::push_back(const T& d) { };
void fct(vector<string>& v) {
int n = v.size();
v.push_back("lee");
}
우리가 template 이 사용된 함수를 call 하면, compiler 는 사용된 type 과 template 에 근거한 함수를 생성한다.
typename 대신 class 를 사용할수있고, 이 둘은 정확히 똑같다.
뭘 사용할지는 개발자가 직접 결정하면 된다.
저자는 class 를 더 선호한다. (짧아서)
template<typename T>;
template<class C>;
19.3.2 Generic programming (일반화 프로그래밍)
일반화 프로그래밍 (generic programming) 의 의미는 매우 간단하다.
"template 을 사용하는것" 이다.
class 에 template 을 사용하면 class template 또는 parameterized type 또는 parameterized class 라고 부른다.
함수에 사용하면 function template 또는 parameterized function 또는 algorithm 이라고 한다.
따라서 일반화 프로그래밍은 algorithm-oriented programming 이라고도 불린다.
즉, design 의 focus 가 data type 보다 algorithm 에 초점을 맞춘것이다.
parameterized type 의 개념은 프로그래밍의 중심에 있다.
template parameter 에 기반한 일반화 프로그래밍의 형태를 parametric polymorphism 이라고한다.
이와 대조적으로, class 계층구조와 virtual 함수를 기반으로한 프로그래밍의 형태는 ad hoc polymorphism 이라고 하며, object-oriented programming 이라고 한다.
두 스타일이 모드 polymorphism 을 포함하는 이유는,
둘다 프로그래머가 single interface 로 다양한 버전의 concept 을 표현하도록 해주기때문이다.
polymorphism 은 many shapes, 즉 공통된 interface 로 서로 다른 type 을 형성할수있다는것이다.
우리는 앞서 Shape 라는 interface 를 통해 Text, Circle, Polygon 등을 만들어냈었다.
우리는 vector 라는 template 을 통해 vector<int>, vector<double> 등을 만들었다.
이런것들이 모두 다형성(polymorphism) 이다.
정리하면,
-
object-oriented-programming : class 계층구조와 virtual 함수를 사용하는것.
-
generic programming : template 을 사용하는것
이 둘의 또다른 매우 중요한 차이점이 있다.
-
object-oriented-programming : 호출되는 함수의 선택이 run-time 에 결정된다.
-
generic programming : compile-time 에서 결정된다.
그래서 template 을 언제 사용해야합니까?
-
성능이 중요하면 template 을 사용하라.
-
여러개의 type 으로부터 공통된 정보를 표현하고자한다면 template 을 사용하라. ex) standard library
19.3.3 Concepts
모든 장점은 해당하는 약점을 가지고있다.
template 의 가장 큰 문제는 template(definition) 과 interface(declaration)
즉, 선언과 정의가 분리되지못한다는것이다.
현재의 compiler 는 template 안에 모든 정보가 있도록 요구한다.(member function 과 template function 들)
이에 의해, 개발자들은 template 의 definition 을 header 파일에 하는 경향이 있다.
이것이 standard 는 아니지만, 다른 방식이 폭넓게 사용되기전까지는 header 파일에서 정의하는것을 권장한다.
한가지 유용한 개발 테크닉은 우리가 vector 를 만들어본것과 비슷하다.
-
특정 type 을 위한 class 를 만들고 테스트 한다.
-
잘 작동한다면, 이 특정 type 을 template 파라미터로 변경하고, 다양한 type 으로 테스트해본다.
Chapter 20, 21 에서 stand library 를 이용한 container 와 algorithm 을 설명하면서
template 사용의 좋은 예들을 접하게 될것이다.
C+14 에서는 template interface 를 향상시킬수있는 메카니즘을 제공한다.
예를 들어, C+11 에서 아래같이 작성한 경우를 보자.
template<typename T>
class vector {
};
우리는 type T 에서 우리가 기대하는 type 이 무엇인지 표현할수있는 방법이 없다.
C+14 에서는 아래와 같이 표현할수있다.
template <typename T>
requires Element<T>() // T is an Element
짧게하면,
template<Element T>
concept 을 사용할수있는 C++14 Compiler 를 가지고있지않다면,
name 과 comment 를 통해 작성하도록한다.
template<typename Elem> // requires Element<elem>()
class vector {
}
compiler 는 Elem 이나 우리가 쓴 comment 를 이해하지못하지만, code 를 봤을때 의미를 알아볼수있다.
concept 으로 사용할수있는 목록은 아래와 같다.
19.3.4 Containers and inheritance
앞서 우리는 base class 의 객체가 들어갈 자리에 파생 class 의 객체를 사용할수있다고 배웠다.
하지만 아래와같은건 안된다.
vector<Shape> vs;
vector<Circle> vc;
vs = vc; // error : vector<Shape> required
void f(vector<Shape>&);
f(vc); // error : vector<Shape> required
Circle 을 Shape 로 바꿀수있지않나요? 라고 묻는다면 잘못된 질문이다.
Circle* -> Shape* 할수없다.
Circle& -> Shape& 로 할수없다.
요악하면, template 은 상속에 사용될수없다.
이것을 기억하라. D is B 라는것이 C<D> 가 C<B> 임을 의미하지않는다는것이다.
19.3.5 Integers as template parameters
앞서 보았듯이, class 를 type 으로 parameterize (매개변수화) 하는것은 확실히 유용하다.
그럼 int 나 string value 같은것들도 parameterize 하는것은 어떨까?
int 외에 다른것을 parameterize 하는것은 생각보다 useful 하지않아서 고려하지않는게 좋다.
int 를 template 의 인자로 넘기는것은 매우 자주쓰이는 패턴이다.
예를 보자.
template<typename T, int N> struct array {
T elem[N]; // hold elements in member array
// rely on default constructors, destructor, and assignment
T& operator[] (int n); // access : return reference
const T& operator[] (int n) const;
T* data() { return elem; } // conversion to T*
const T* data() const { return elem; }
int size() const { return N; }
};
위와 같이 정의하고 아래와 같이 사용할수있다.
array<int, 256> gb; //
array<double, 6> ad = {0. 0, 1.1, 2.2, 3.3, 4.4, 5.5};
const int max = 1024;
void some_fct(int n) {
array<char, max> loc;
array<char, n> oops; // error : the value of n not known to compiler
array<char, max> loc2 = loc;
loc = loc2;
}
확실히 array 는 vector 보다 단순하지만 powerful 하지않다.
그럼 왜 누군가는 array 를 vector 대신 사용하는것을 원할까?
이유는 efficieny (효율성) 이다.
array 는 compile-time 에 자신의 size 를 알고있기에,
compiler 는 static 메모리에 global 객체를, stack 메모리에 local 객체를 allocate 할수있다. (free store 를 쓰지않고)
대부분의 프로그램에서 이런 효율성 향상은 그다지 중요하지않다.
하지만 당신이 네트워크 driver 같은 매우 중요한 시스템 요소를 만들고있다면,
작은 차이도 문제가 된다.
또, embedded 시스템이나 보안 관련 프로그램들은 free store 를 사용할수없게 되어있다.
이런 프로그램에게 array 는 no free-store use 라는 vector 가 가지고있지않는 많은 장점을 가질수있다.
하지만 반대로, array 는 잘못작동하기 쉽다.
자신의 size 를 모르고, pointer 를 변환하며, copy 를 적절하게 수행하지못한다.
19.3.6 Template argument deduction
class template 을 사용할때 우리는 객체를 생성할때 template argument 를 특정할수있다.
array<char, 1024> buf;
array<double, 10> b2;
함수 template 을 사용할때는, compiler 는 template argument 를 함수 argument 로부터 추정한다.
template<class T, int N> void fill(array<T, N>& b, const T& val) {
for (int i = 0; i < N; ++i) {
b[i] = val;
}
}
void f() {
fill(buf, 'x'); // fill<char, 1024>(buf, 'x') 로 적어야하지만 compiler 가 추정함.
fill(b2, 0.0); // fill<double, 10>(b2, 0) 로 적어야야하지만 compiler 가 추정함.
}
19.3.7 Generalizing vector
생략.
19.4 Range checking and exceptions
at() 을 사용해서 vector 의 out-of-range 를 catch 하라는 말씀.
19.4.1 An side : design considerations
19.4.1.1 Compatibility (호환성)
19.4.1.2 Efficiency (효율성)
19.4.1.3 Constraints (강제)
19.4.1.4 Optional checking
요약 : 언제나 exception based error handling 과 range-checked vector 를 사용하라.
19.4.2 A confession : macros
macro 를 절대 사용하지말라.
#define vector Vector // 이런게 macro
19.5 Resources and exceptions
프로그래밍의 기본적인 원칙은 "사용하였다면 반드시 돌려줘라" 이다.
여기서 사용하고 돌려줘야할것은 resource 이다.
resource 란 아래와 같은것들이다.
-
Memory
-
Locks
-
File handles
-
Thread hanels
-
Sockets
-
Windows
19.5.1 Potential resource management problems
생략.
19.5.2 Resource acquisition is initialization (RAII)
언제나 기본은 constructor 로 acquire 하고, destructor 로 release 하는것이다.
new 와 delete 대신 vector 를 사용하라.
19.5.3 Guarantees
19.5.4 unique_ptr
unique_ptr 은 일종의 pointer 이다.
하지만 unique_ptr 은 자신이 가리키는 객체를 own (소유) 한다.
따라서, unique_ptr 이 파괴될때, 자신이 가리키던 객체 또한 delete 한다.
unique_ptr 은 또한 하나의 객체를 또다는 unique_ptr 로 가리키게 할수없다.
아래와 같은 코드는 error 이다.
void no_good() {
unique_ptr<X> p { new X};
unique_ptr<X> q { p }; // error : fortunately
}
// scope 가 끝나면, p 가 가리키던 객체 X 는 파괴.
이런 포인터를 smart pointer 라고 한다.
만약 pointer 의 copy 가 가능하고, 자동으로 삭제를 보장해주는 pointer 를 원한다면 shared_ptr 을 사용하라.
(하지만 사용하지않도록 하라...)
unique_ptr 이 다른 pointer 들과 비교해서 가장 좋은 놈임.
19.5.5 Return by moving
19.5.6 RAII for vector
생략이지만..나중에 읽어볼것.. (이해가 안됨, 대갈통 깨질것같음)
Review
- Why would we want to change the size of a vector?
- Why would we want to have difference element types for different vectors?
- Why don't we just always define a vector with a large enough size of all eventualities?
- How much spare space do we allocate for a new vector?
- When must we copy vector elements to a new location?
- Which vector operations can change the size of a vector after construction?
- What is the value of a vector after a copy?
- Which two operations define copy for vector?
- What is the default meaning of copy for class objects?
- What is a template?
- What are the two most useful types of template arguments?
- What is generic programming?
- How does generic programming differ from object-oriented programming?
- How does array differ from vector?
- How does array differ from the built-in array?
- How does resize() differ from reserve()?
- What is a resource? Define and give examples.
- What is a resource leak?
- What is RAII? What problem does it address?
- What is unique_ptr good for?
Terms
- #define
- at()
- basic guarantee
- exception
- guarantees
- handle
- instantiation
- macro
- owner
- push_back()
- RAII
- resize()
- resource
- re-throw
- self-assignment
- shared_ptr
- specialization
- strong guarantee
- template
- template parameter
- this
- throw;
- unique_ptr