음악, 삶, 개발

19. Vector, Templates, and Exceptions 본문

개발 공부/Principles And Practice Using C++

19. Vector, Templates, and Exceptions

Lee_____ 2020. 7. 28. 00:28

Intro

 

이 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 가 있다고 가정해보자.

서로 size 가 다른 vector

우리는 v1 에 v2 를 복사하고싶다.

v1 = v2;

v2 를 v1 에게 복사했을때.

우리가 직접 이 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 을 언제 사용해야합니까?

  1. 성능이 중요하면 template 을 사용하라. 

  2. 여러개의 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 를 만들어본것과 비슷하다.

  1. 특정 type 을 위한 class 를 만들고 테스트 한다.

  2. 잘 작동한다면,  이 특정 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 으로 사용할수있는 목록은 아래와 같다.

 

C++14 : 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

 

  1. Why would we want to change the size of a vector?
  2. Why would we want to have difference element types for different vectors?
  3. Why don't we just always define a vector with a large enough size of all eventualities?
  4. How much spare space do we allocate for a new vector?
  5. When must we copy vector elements to a new location?
  6. Which vector operations can change the size of a vector after construction?
  7. What is the value of a vector after a copy?
  8. Which two operations define copy for vector?
  9. What is the default meaning of copy for class objects?
  10. What is a template?
  11. What are the two most useful types of template arguments?
  12. What is generic programming?
  13. How does generic programming differ from object-oriented programming?
  14. How does array differ from vector?
  15. How does array differ from the built-in array?
  16. How does resize() differ from reserve()?
  17. What is a resource? Define and give examples.
  18. What is a resource leak?
  19. What is RAII? What problem does it address?
  20. 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