AcceleratedC++/Chapter13 AcceleratedC++/Chapter15


1. Chapter 14 Managing memory (almost) automatically

Student_info 클래스는 레코드의 인터페이스. 레코드의 메모리 공간을 관리한다.
이렇게 2가지의 추상적인 기능을 조합해서 만들게 되는 것은 허술한 클래스 설계때문인 경우가 많다.

이 장에서는 포인터처럼 동작하지만, 자체 메모리 관리를 포함하는 클래스를 작성하려고 합니다.
내부의 한 개체를 가리키는 포인터와 비슷한 객체를 적절히 사용하면 불필요한 복사가 행해지는 성능상의 문제를 해결할 수 있다.

만약 객체 x가 y를 가리킨다면 x를 복사한다고 해서 y도 볼사될까?
만약 y가 x의 멤버라면 이는 옳다. 그렇지만 어쩌다가 y를 가리키게 된 경우라면 이는 옳지 않다.

이장의 내용은 상당히 추상적이기 때문에 상당히 주의 깊은 이해가 필요하다.

1.1. 14.1 Handles that copy their objects

13장에서 문제를 해결하기위해서는 서로 다른 타입의 객체를 한개의 컬렉션에 젖하는 방법이 필요했다.
13.3.1절의 첫번째 해결법에선느 이를 위해서 포인터를 사용하여서 Core 혹은 Core로 부터 파생된 객체들을 생성하여 컬렉션 내부의 포인터들로 가리키도록 하였다. 따라서 이 경우 사용자 코드는 객체의 동적생성, 해제에 관련된 것들을 처리할 책임이 있었다.

저수준 자료구조인 포인터를 직접상요함으로서 생기는 문제점
* 포인터를 복사하는 것은 그 대상 객체를 복사하는 것과는 다름.
* 포인터의 소멸이 그 객체의 소멸을 의미하지 않는다. (memory leak)
* 포인터 소멸시키지 않고, 객체를 소멸할경우 dangling pointer 발생.
* 포인터 생성시 초기화하지 않으면, 포인터를 바인딩되지 않은 상태가된다.
마지막 2가지의 경우는 그 포인터를 다른 곳에서 참조할 경우 어떤 일이 일어날지 알 수 없다.

13.5의 Student_info 는 프로그래머가 내부의 Core객체를 볼 수없고, 자동으로 메모리 관리가 되도록은 했으나, 메소드들이 Core클래스의 public연산들을 그대로 따르는 것들이 많다.
이장에서는 이런 핸들(handle)클래스를 일반적인 방식으로 관리하는 것을 알게 된다.

1.1.1. 14.1.1 제네릭 핸들 클래스(generic handle class)

이 클래스는 객체의 형에 무관한게 동작하여야 하므로 템플릿으로 작성한다.
Handle 클래스의 요구사항
* Handle은 객체의 참조값
* Handle은 복사가 가능하다
* Handle 객체가 다른 객체에 바인딩되어 있는지 확인이 가능
* Handle클래스가 가리키는 객체가 상속구조의 클래스형을 가리킨다면 virtual 에 의해 지정된 연산에대해서 다형성을 제공한다.

사용자가 Handle 클래스를 이용해서 특정한 개체에 Handle을 붙이게 되면 Handle은 그 객체의 메모리를 관리하게 된다.
대신 일단 그 객체는 오직 하나의 Handle만을 부착시켜야 하며, 일단 부착시킨 뒤에는 포인터가아닌 Handle을 이요해서 객체에 접근해야한다.
즉 Handle이 소멸되면 Handle이 가리키는 객체도 소멸되게 된다. 사용자는 바인딩이 안된 객체를 가리키는 핸들을 만들수는 있지만 이 경우 핸들에 접근하게되면 예외 상황을 발생하게된다. (아니면 처음 생성시 객체가 바인딩 되어있는지를 검사하도록 하면 된다.)
~cpp 
template <class T> class Handle {
public:
	Handle(): p(0) { }	// 기본생성자는 내부 멤버를 0으로 초기화하여서 아직 바인딩이 안된 상태임을 나타낸다.
	Handle(const Handle& s) : p(0) { if (s.p) p = s.p->clone(); }	// 복사 생성자는 인자로 받은 객체의 clone() 함수를 통해서 새로운 객체를 생성하고 그 것을 대입한다.
	Handle& operator=(const Handle&);
	~Handle() { delete p; }

	Handle(T* t):p(t) { }

	operator bool() const { return p; }
	T& operator*() const;
	T* operator->() const;

private:
	T* p;
};

template<class T> Handle<T>& Handle<T>::operator=(const Handle& rhs) {
	if(&rhs != this) {		// 자기 참조를 조사한뒤 행동방식을 결정한다.
		delete p;
		p = rhs.p ? rhs.p->clone() : 0;
	}
	return *this;
}
 
~cpp 
 Handle<Core> p(new Grad);	// Handle<Core> ==  Core*
				// Handle<Core> 는 새로 생성된 Grad를 가리킨다.
 

~cpp 
template<class T> T& Handle<T>::operator*() const {
	if(p)
		return *p;
	throw runtime_error("unbound Handle");
}	// 최기화를 할때 생성한 객체를 가리키는 결과를 리턴한다.

template<class T> T* Handle<T>::operator->() const {
	if(p)
		return p;
	throw runtime_error("unbound Handle");
}
 
-> 연산자는 일견 이항 연산자 처럼 보이지만 동작하는 방식이 다른 연산자들과는 다르다. ->를 호출하게 되면 연산자의 좌측요소에서 포인터를 대신해서 사용이 가능한 요소가 리턴된다.

~cpp 
// 동일한 표현
x->y;
(x.operator->())->y;

//as upper
student->y;
(student.operator->())->y;
student.p->y;
 
상기의 정의에서 *, -> 연산자를 참조형, 포인터형으로 리턴하게 함으로써 자동으로 파생객체에 대한 동적바인딩이 가능해 진다.

1.1.2. 14.1.2 제네릭 핸들 사용하기


~cpp 
//main.cpp
#include <algorithm>
#include <ios>
#include <iomanip>
#include <iostream>
#include <stdexcept>
#include <vector>

#include "Handle.h"
#include "Student_info.h"

using std::cin;
using std::cout;
using std::domain_error;
using std::endl;
using std::sort;
using std::streamsize;
using std::setprecision;
using std::setw;
using std::string;
using std::vector;

using std::max;

bool compare_Core_handles(const Handle<Core>& lhs, const Handle<Core>& rhs) {
	return compare(*lhs, *rhs);
}

int main()
{
	vector< Handle<Core> > students;       // changed type
	Handle<Core> record;                   // changed type
	char ch;
	string::size_type maxlen = 0;

	// read and store the data
	while (cin >> ch) {
		if (ch == 'U')
			record = new Core;      // allocate a `Core' object
		else
			record = new Grad;      // allocate a `Grad' object

		record->read(cin);  //  `Handle<T>::->', then `virtual' call to `read'
		maxlen = max(maxlen, record->name().size()); // `Handle<T>::->'
		students.push_back(record);
	}

	// `compare' must be rewritten to work on `const Handle<Core>&'
	sort(students.begin(), students.end(), compare_Core_handles);

	// write the names and grades
	for (vector< Handle<Core> >::size_type i = 0;
	     i != students.size(); ++i) {
		// `students[i]' is a `Handle', which we dereference to call the functions
		cout << students[i]->name()
		     << string(maxlen + 1 - students[i]->name().size(), ' ');
		try {
			double final_grade = students[i]->grade();
			streamsize prec = cout.precision();
			cout << setprecision(3) << final_grade
			     << setprecision(prec) << endl;
		} catch (domain_error e) {
			cout << e.what() << endl;
		}
		// no `delete' statement
	}
	return 0;
}
 
Handle 클래스는 연결된 객체의 clone() 멤버함수를 이용한다. 따라서 Core클래스에 clone()메소드를 public으로 작성하는 것이 필요하다.

Handle<>을 이용한 Student_info 의 구현
~cpp 
//Student_info.h
#ifndef GUARD_Student_info
#define GUARD_Student_info

#include <iostream>
#include <string>

#include "Core.h"
#include "Handle.h"

class Student_info {
public:
	Student_info() { }
	Student_info(std::istream& is) { read(is); }
	// no copy, assign, or destructor: they're no longer needed

	std::istream& read(std::istream&);

	std::string name() const {
		if (cp) return cp->name();
		else throw std::runtime_error("uninitialized Student");
	}

	double grade() const {
		if (cp) return cp->grade();
		else throw std::runtime_error("uninitialized Student");
	}
	static bool compare(const Student_info& s1,
	                    const Student_info& s2) {
		return s1.name() < s2.name();
	}
private:
	Handle<Core> cp;	// Handle 클래스가 생성과 소멸을 자동화하기 때문에 복사 생성자, 대입 연산자, 소멸자가 필요 없다.
};
#endif
 

Student_info::read 의 재정의
~cpp 
//Student_info.cpp
#include <iostream>

#include "Student_info.h"
#include "Core.h"

using std::istream;

istream& Student_info::read(istream& is)
{
	char ch;
	is >> ch;     // get record type

	// allocate new object of the appropriate type
	// use `Handle<T>(T*)' to build a `Handle<Core>' from the pointer to that object
	// call `Handle<T>::operator=' to assign the `Handle<Core>' to the left-hand side
	if (ch == 'U')
		cp = new Core(is);
	else
		cp = new Grad(is);

	return is;
}
 
자동으로 객체의 메모리 관리가 되기 때문에 delete 구문을 제거함. Handle::operator=()에 내부 메모리의 해제 구문이 들어있기 때문에 불필요.

1.2. 14.2 Reference-counted handles

어떤 경우에 프로그래머는 Handle이 대상 객체를 복사하는 형태가 아니라 단지 가리키는 형태로만 사용되기를 바랄 수 있다. 즉 동일한 객체를 2개의 다른 Handle 이 가리킬 수 있다는 말이다.
이경우 대상객체의 해제는 객체를 가리키는 마지막 핸들이 소멸될때 행해져야한다. 이를 위해 레퍼런스 카운트(reference count, 참조계수)를 사용한다.
이를 위해서 우리는 이전의 클래스에 카운터(counter, 계수기)를 추가하고 객체의 생성, 복사, 소멸시 이 카운터를 적절하게 갱신한다.
~cpp 
//Ref_handle.h
#ifndef Ref_handle_h
#define Ref_handle_h

#include <cstddef>
#include <stdexcept>

template <class T> class Ref_handle {
public:
	// manage reference count as well as pointer
	Ref_handle(): p(0), refptr(new size_t(1)) { }
	Ref_handle(T* t):  p(t), refptr(new size_t(1)) { }
	Ref_handle(const Ref_handle& h): p(h.p), refptr(h.refptr) {
		++*refptr;
	}	// 참조형으로 동작하는 Handle인경우. 카운터의 주소를 대상객체의 카운터의 주소로 초기화, 카운터를 증가시킨다.

	Ref_handle& operator=(const Ref_handle&);
	~Ref_handle();

	// as before
	operator bool() const { return p; }
	T& operator*() const {
		if (p)
			return *p;
		throw std::runtime_error("unbound Ref_handle");
	}
	T* operator->() const {
		if (p)
			return p;
		throw std::runtime_error("unbound Ref_handle");
	}

private:
	T* p;
	std::size_t* refptr;      // added
};

template <class T>
Ref_handle<T>& Ref_handle<T>::operator=(const Ref_handle& rhs)
{	// Ref_handle&를 인자로갖는 생성자와 마찬가지로 operator= 도 인자로 받은 대상이 Ref_handle 인 경우 카운터를 하나 증가시킨다.
	++*rhs.refptr;

	// free the left-hand side, destroying pointers if appropriate
	// 여기서 만약 자기 자신을 대입할 경우에는 객체의 참조의 카운터가 변화하지 않게된다.
	if (--*refptr == 0) {
		delete refptr;
		delete p;
	}	// 마지막 대상객체의 핸들이라면 현재 존재하는 대상 객체를 삭제

	// copy in values from the right-hand side
	refptr = rhs.refptr;
	p = rhs.p;
	return *this;
}

template <class T> Ref_handle<T>::~Ref_handle()
{
	if (--*refptr == 0) {
		delete refptr;
		delete p;
	}
}	// 소멸되는 핸들이 대상객체를 가리키는 마지막 객체라면 메모리에서 해제
	// refptr도 동적할당된 객체이므로 메모리에서 해제해주는 코드가 필요하다.

#endif

문제점
~cpp 
//Ref_handle을 기반으로 작성된 Student_info 클래스의 사용시
Student_info s1(cin);
Student_info s2 = s1;	// s1의 값을 s2로 복사한다. 하지만 내부의 객체는 같은 객체를 가리킨다.
필요없는 복사는 일어나지 않지만 이 경우 프로그래머가 원치 않을 경우에도 동일한 객체를 가리키는 일이 발생한다.

1.3. 14.3 Handles that let you decide when to share data

~cpp 
//ptr.h
#ifndef GUARD_Ptr_h
#define GUARD_Ptr_h

#include <cstddef>
#include <stdexcept>

template <class T> class Ptr {
public:
	// new member to copy the object conditionally when needed
	void make_unique() {
		if (*refptr != 1) {
			--*refptr;
			refptr = new size_t(1);
			p = p? clone(p): 0;
		}
	}
	/*
	이 함수는 프로그래머가 필요할때 현재 가리키는 내부객체와 
	동일한 내용의 객체를 복사한 객체를 만들어준다.

	대신에 가리키는 대상의 핸들이 1개인 경우에는 이런 복사를 행하지 않는다.
	*/
	// the rest of the class looks like `Ref_handle' except for its name
	Ptr(): p(0), refptr(new size_t(1)) { }
	Ptr(T* t): p(t), refptr(new size_t(1)) { }
	Ptr(const Ptr& h): p(h.p), refptr(h.refptr) { ++*refptr; }

	Ptr& operator=(const Ptr&);    // implemented analogously to 14.2/261
	~Ptr();                        // implemented analogously to 14.2/262
	operator bool() const { return p; }
	T& operator*() const;          // implemented analogously to 14.2/261
	T* operator->() const;         // implemented analogously to 14.2/261

private:
	T* p;
	std::size_t* refptr;
};

template <class T> T* clone(const T* tp)
{
	return tp->clone();
}

template<class T>
T& Ptr<T>::operator*() const { if (p) return *p; throw std::runtime_error("unbound Ptr"); }

template<class T>
T* Ptr<T>::operator->() const { if (p) return p; throw std::runtime_error("unbound Ptr"); }

template<class T>
Ptr<T>& Ptr<T>::operator=(const Ptr& rhs)
{
        ++*rhs.refptr;
        // \f2free the lhs, destroying pointers if appropriate\fP
        if (--*refptr == 0) {
                delete refptr;
                delete p;
        }

        // \f2copy in values from the right-hand side\fP
        refptr = rhs.refptr;
        p = rhs.p;
        return *this;
}

template<class T> Ptr<T>::~Ptr()
{
        if (--*refptr == 0) {
                delete refptr;
                delete p;
        }
}
#endif
이 구조를 이용해서 Student_info를 작성하는 경우 우리는 새로 이 클래스에 대해서 작성할 코드가 전혀없다.
왜냐하면 Student_info에 대해서 내부 객체를 변경하는 함수는 오직 read인데 우리의 경우에는 read 함수 호출시 기존의 내부 멤버를 소멸시키고, 다시 객체를 만들어서 할당하기 때문이다.
~cpp 
Student_info s1;
s1.read(cin);
Student_info s2 = s1;
s2.read(cin)
그런데 내부객체인 Ptr 핸들은 그 요소를 나타내는 핸들이 오직 1개일 경우가 아니면 대상의 메모리를 해제 하지 않기 때문에 아래와 같은 코드에서 s1, s2의 값의 변화가 상호 영향을 미치지 않는다.

호출 객체의 변화가 같은 요소를 가리키는 핸들들에게 영향을 주지않기를 바라는 regrade 함수
~cpp 
void Student_info::regrade(double final, double thesis) {
	cp.make_unique();

	if(cp)
		cp->regrade(final, thesis);
	else throw run_time_error("regrade of unknown student");
}

1.4. 14.4 An improvement on controllable handles

~cpp 
//Str.h - 치기가 귀찮아서 그냥 복사함. -_-
#ifndef GUARD_Str_h
#define GUARD_Str_h

#include <algorithm>
#include <iostream>
#include "Ptr.h"
#include "Vec.h"

template<>
Vec<char>* clone(const Vec<char>*);

// does this version work?
class Str {
	friend std::istream& operator>>(std::istream&, Str&);
	friend std::istream& getline(std::istream&, Str&);

public:
	Str& operator+=(const Str& s) {
		data.make_unique();
		std::copy(s.data->begin(), s.data->end(),
		          std::back_inserter(*data));
		return *this;
	}

	// interface as before
	typedef Vec<char>::size_type size_type;

	// reimplement constructors to create `Ptr's
	Str(): data(new Vec<char>) { }
	Str(const char* cp): data(new Vec<char>)  {
		std::copy(cp, cp + std::strlen(cp),
		          std::back_inserter(*data));
	}

	Str(size_type n, char c): data(new Vec<char>(n, c)) { }
	template <class In> Str(In i, In j): data(new Vec<char>) {
		std::copy(i, j, std::back_inserter(*data));
	}

	// call `make_unique' as necessary
	char& operator[](size_type i) {
		data.make_unique();
		return (*data)[i];
	}
	const char& operator[](size_type i) const { return (*data)[i]; }
	size_type size() const { return data->size(); }

	typedef char* iterator;
	typedef const char* const_iterator;

	iterator begin() { return data->begin(); }
	const_iterator begin() const { return data->begin(); }

	iterator end() { return data->end(); }
	const_iterator end() const { return data->end(); }

private:
	// store a `Ptr' to a `vector'
	Ptr< Vec<char> > data;
};
// as implemented in 12.3.2/216 and 12.3.3/219 
std::ostream& operator<<(std::ostream&, const Str&);
Str operator+(const Str&, const Str&);
inline bool operator<(const Str& lhs, const Str& rhs)
{
        return std::lexicographical_compare(lhs.begin(), lhs.end(), rhs.begin(), rhs.end());
}
inline bool operator>(const Str& lhs, const Str& rhs)
{
        return std::lexicographical_compare(rhs.begin(), rhs.end(), lhs.begin(), lhs.end());
}
inline bool operator<=(const Str& lhs, const Str& rhs)
{
        return !std::lexicographical_compare(rhs.begin(), rhs.end(), lhs.begin(), lhs.end());

}
inline bool operator>=(const Str& lhs, const Str& rhs)
{
        return !std::lexicographical_compare(lhs.begin(), lhs.end(), rhs.begin(), rhs.end());

}
inline bool operator==(const Str& lhs, const Str& rhs)
{
        return lhs.size() == rhs.size() &&
                std::equal(lhs.begin(), lhs.end(), rhs.begin());
}
inline bool operator!=(const Str& lhs, const Str& rhs)
{
        return !(lhs == rhs);
}
#endif
기본 인터페이스는 이전의 Str과 동일하지만 자료구조가 Ptr< Vec<char> > 형으로 정의되었기 때문에 전체적인 메소드의 세부 구현이 많이 변경되었다. 그리고 Ptr템플릿으로 자료구조를 다룸으로서 Str클래스가 동일한 객체를 가리킬 수 있는 기능을 제공한다.

1.4.1. 14.4.1 제어할 수 없는 타입 복사하기


~cpp 
template<class T> void Ptr<T>::make_unique() {
	if(*refptr != 1) {
		--*refptr;
		refptf = new size_t(1);
		p = p? p->clone() : 0;
	}
}
 
이 구현을 위해서는 Vec::clone()가 정의되어 있어야하지만, 이 함수를 정의하게 될 경우 원래 Vec의 구현이 표준 함수 vector의 구현의 부분이라는 가정에서 위배되기 때문에 추가할 수는 없다.
이를 해결하기 위해서 우리는 전역함수인 clone()를 만들어서 해결한다. (소프트웨어 공학에서는 한단계를 우회하면 모든 문제가 해결된다라는 말이 있는데 여기에 적용될 수 있다,)
~cpp 
template<class T> void Ptr<T>::make_unique() {
	if(*refptr != 1) {
		--*refptr;
		refptf = new size_t(1);
		p = p? clone() : 0;
	}
}
 

::clone()의 구현
~cpp 
template<> Vec<char*> clone(const Vec<char>* vp) {
	return new Vec<char>(*vp);
}
 
템플릿의 구체화(template specialization)
template<>를 사용하면 특정 인자 타입에 대한 특정 템플릿 함수의 버전을 정의한다.
이렇게 만들어진 함순느 만약 Vec<char>*가 인자로 오는 경우에는 이 것이 실행되고 다른 버전의 템플릿이 오는 경우에는 그 객체의 clone()버전을 실행하게 된다.

이러한 사항을 요약하면 다음과 같다
* Ptr<T>::make_unique()를 사용하지 않는다면 T::clone은 불필요
* Ptr<T>::make_unique를 사용한다면 T::clone가 있다면 T::clone을 사용할 것이다.
* Ptr<T>::make_unique를 사용한다면 T::clone가 미정의되었다면 clone<T>를 정의해서 원하는 바를 얻을 수 있다.

1.4.2. 14.4.2 언제 복사가 필요할까요?

만약 const 객체를 통해서 operator[]를 통해서 접근한다면 객체의 내용을 바꾸는 것을 허용해서는 안된다.
이럴경우 operator[] const를 통해서 리턴되는 값은 make_unique를 통해서 복사된 것을 리턴함으로서 원본 객체의 데이터의 변형을 방지하는 것이 가능하다.

Retrieved from http://wiki.zeropage.org/wiki.php/AcceleratedC++/Chapter14
last modified 2021-02-07 05:22:24