AcceleratedC++/Chapter11 AcceleratedC++/Chapter13


1. Chapter 12 Making class objects act like values

일반적인 C++의 기본형 데이터처럼 클래스도 여러가지의 연산자를 재정의 함으로써 마치 값처럼 동작하도록 할 수 있다.
이 장에서는 여러가지 연산자, 형변환을 클래스의 제작자가 제어하는 방법에 대해서 배운다.
12장에서는 string 클래스의 클론 버전인 Str 클래스를 제작한다. Str 클래스는 string 클래스의 형변환과 연산자의 구현에 초점을 맞추어서 제작한다.

1.1. 12.1 A simple string class

~cpp 
class Str {
public:
	typedef Vec<char>::size_type size_type;
	Str() {}
	Str(size_type n, char c):data(n, c) { }
	Str(const char* cp) {
		std::copy(cp, cp+std::strlen(cp), std::back_inserter(data));
	}
	
	template<class In> Str(In b, In e) {
		std::copy(b, e, std::back_inserter(data));
	}
private:
	Vec<char> data;
}
만약 생성자가 하나라도 존재하면 컴파일러는 암시적 기본 생성자를 만들지 않는다. 따라서 아무런 인자를 받지 않는 기본 생성자를 만들 필요가 있다.
이 클래스는 복사 생성자, 대입 연산자, 소멸자가 하는 모든일을 Vec클래스에 일임한다.

1.2. 12.2 Automatic conversions

~cpp 
Str s("hello");		// s를 생성한다. 
Str t = "hello";	// t를 초기화 한다. 
s = "hello";		// 새로운 값을 s에 대입한다. 
클래스는 기본적으로 복사 생성자, 대입 연산자의 기본형을 제공한다. 위의 클래스는 이런 연산에 대한 기본적인 요건을 만족하기 때문에 const char* 가 const Str& 로 변환되어서 정상적으로 동작한다.
상기의 클래스에는 Str(const char*) 타입의 생성자가 존재하기 때문에 이 생성자가 Str 임시 객체를 생성해서 마치 사용자 정의 변환(user-define conversion)처럼 동작한다.

~cpp 
s="hello" 
s=Str("hello");		// 의 임시객체를 만들어서 만들어진 임시객체가 디폴트 복사 생성자를 통해서 할당되게 된다. 


1.3. 12.3 Str operations

string 에서 가능했던 연산들
~cpp 
cin>>s;		
cout<<s;	
s[i];		
s1 + s1		

operator[] implementation
~cpp 
class Str{
public:
	// 이전의 생성자들 
	char& operator[](size_type i) { return data[i]; }
	const char& operator[](size_type i) const { return data[i]; }
private:
	Vec<int> data;
};
이 구현의 세부적인 작동방식은 모두 Vec 클래스로 위임하였다. 대신에 const 클래스와 const 가 아닌 클래스에 대한 버전을 제공하였고, 표준 string 함수와의 일관성 유지를 위해서 string 대신에 char& 형을 리턴하도록 하였음.

1.3.1. 12.3.1 입출력 연산자들

입력 연산자는 일견 객체의 상태를 바꾸기 때문에 멤버함수로 선언이 되어야 한다고 생각하기 쉽다. 그러나 이항 연산자의 경우 파라메터의 맵핑이 좌항의 경우 첫번째 우항의 경우 두번째인자로 받는데, 이렇게 될 경우 멤버함수로 >>연산자를 오버로딩하면 우리가 워하는 결과를 얻을 수 없다.
~cpp 
cin>>s;
cin.operator>>(s);		// istream 을 우리가 만든 객체가 아니기 때문에 재정의할 수 없다.
s.operator>>(cin);		// 표준 istream 의 형태가 아니다.
s>>cin
 
상기와 같은 이유로 operator>>는 비멤버함수로서 선언이 되어야 한다.

~cpp 
std::istream& operator>>(std::istream&, Str&);
std::ostream& operator<<(std::ostream&, const Str&);

ostream& operator<<(ostream& os, const Str& s) {
	for(Str::size_type i=0; i!= s.size(); ++i)
		os<<s[i];
	return os;
}
 
위의 식 처럼 사용하기 위해서는 Str::size() 가 정의 되어야한다.
~cpp 
class Str{
public:
	size_type size() const { return data.size(); }
	// 이전과 동일
}
 

1.3.2. 12.3.2 프렌드(Friend)


~cpp 
istream& operator>>(istream& is, Str& s) {
	s.data.clear();	// compile error. private 멤버로 접근

	char c;
	while(is.get(c) && isspace(c))		// 입력이 있고 값이 공백이라면 무시
	;

	if(is) {	// EOF를 만나면 입력 스트림을 false 값을 리턴한다.
		do s.data.push_back(c);	// compile error. private 멤버로 접근
		while(is.get(c) && !isspace(C));

		if(is)		// 입력중 공백 문자를 만났을 경우 방금전에 입력으로 들어왔던 한문자를 무시한다.
			is.unget();
	}
	return is;
}
 
상기의 함수는 Str 자료형에 입력을 하기 때문에 Str 형에 대한 쓰기 권한이 필요하다. 그러나 9.3.1절처럼 단순히 입력 함수를 만들게 되면 일반 사용자가 객체의 내부 구조를 건드릴 수 있는 인터페이스를 제공하는 꼴이 되기 때문에 옳지 못하다.
따라서 우리는 operator>>를 public 멤버로 만들고 data에 대한 쓰기 권한을 가지게 해서는 안된다.
이런 경우의 함수를 friend 로 정의 하여 해결하는 것이 가능하다.
함수를 friend 로 정의하면 인자로 받은 형에대해서는 형의 접근자가 무시된다.
~cpp 
class Str {
friend std::istream& operator>>(std::istream&, Str&);
};
 
friend 함수는 접근제어 레이블에 영향을 받지 않기 때문에 어디에 선언을 해도 무관하나, 가능하면 클래스 선언의 최초 부분에 놓는 것이 좋다.

1.3.3. 12.3.3 다른 이항 연산자들

operator+ 구상하기
~cpp 
s1 + s2 + s3;
 
  • operator+는 각 인수로 받는 객체의 값을 변화시키지 ㅤㅇㅏㅎ는다. 따라서 비멤버함수로 구현하는 것이 적당하다.
  • operator+는 연산의 결과는 Str 형을 리턴해야한다.

    ~cpp 
     Str operator+(const Str&, const Str&);
     
    그리고 operator+=() 역시도 구현해야할 필요가 있다.
    이런 식을 만족하는 Str선언은 다음과 같다.
    ~cpp 
    class Str {
    	// input operator implemented in 12.3.2/216
    	friend std::istream& operator>>(std::istream&, Str&);
    public:
    	Str& operator+=(const Str& s) {
    		std::copy(s.data.begin(), s.data.end(),
    		          std::back_inserter(data));
    		return *this;
    	}
    
    	// as before
    	typedef Vec<char>::size_type size_type;
    
    	Str() { }
    	Str(size_type n, char c): data(n, c) { }
    	Str(const char* cp) {
    		std::copy(cp, cp + std::strlen(cp), std::back_inserter(data));
    	}
    	template <class In> Str(In i, In j) {
    		std::copy(i, j, std::back_inserter(data));
    	}
    
    	char& operator[](size_type i) { return data[i]; }
    	const char& operator[](size_type i) const { return data[i]; }
    	size_type size() const { return data.size(); }
    
    private:
    	Vec<char> data;
    };
    
    // output operator implemented in 12.3.2/216
    std::ostream& operator<<(std::ostream&, const Str&);
    Str operator+(const Str&, const Str&);
     
    상기에서 구현된 operator+= 는 Vec를 copy함수를 이용해서 구현하였다.
    operator+는 operator+=를 이용해서 다음과 같이 간단하게 구현이 가능하다.

    ~cpp 
    Str operator+(const Str& s, const Str& t) {
    	Str r =s;
    	r += t;
    	return r;
    }
     
    지역변수로 생성된 r를 복사생성자를 통해 생성된 임시 객체로 리턴시킨다.

1.3.4. 12.3.4 혼합-타입 표현식(Mixed-type expression)


~cpp 
Str greeting = "Hello, " + name + "!";
 
상기의 문장은 다음과 동일한 순서로 동작하게 된다.
~cpp 
Str temp1("Hello, ");
Str temp2 = temp1 + name;
Str temp3("!");
Str greeting = temp2 + temp3;
 
이런식으로 동작하게 하면 임시 변수의 생성으로 인한 오버헤드가 상당함으로 알 수 있다. 이런 문제를 해결하기 위해서 string 클래스는 자동변환에 의존하지 않고, 피연산자들의 모든 조합에 대해 결합 연산자를 제공한다.

1.3.5. 12.3.5 이항 연산자 설계하기

이항연산자를 설계할 경우에는 변환(conversion)이 많은 도움이 된다.
이항연산자는 비 멤버함수로 설계하는 것이 좋다. 이유는 멤버함수의 경우 첫번째 인자가 객체의 특정형으로 고정되기 때문에 자동 형변환을 이용할 수 없기 때문이다. 즉 대칭성(symmetry)를 유지하는 것이 가능하다.
멤버함수로 이용되는 경우 우항 피연산자가 자동 형변환되어 좌항과 일치하지 않는 것과 같은 문제가 발생할 수 잇다.
반면 대입연산자는 연산의 결과가 특정 객체에 영향을 주어야 하기 때문에 반드시 클래스의 멤버함수로 작성하는 것이 옳다.

1.4. 12.4 Some conversions are hazardous

11.2.2 에서의 explicit 키워드의 사용은 자동 형변환에 이용이 되는 단일 인자를 받는 생성자의 행위를 제한함으로써 프로그래머가 원치 않는 변환을 막는 기능을 한다.
만약 explicit 으로 Vec 클래스의 Vec<T>::Vec(size_type n) 을 정의 했다고 가정하고 다음의 코드를 보자
~cpp 
Vec<string> p = frame(42);
원래의 의도 : 42라는 글자에 테두리가 처진 결과
실행 : 42개의 빈 행에 테두리가 그려진 결과

일반적으로 객체의 구조를 결정하는 생성자는 explicit 으로 내용을 구성하는 경우에는 암묵적인 실행이 가능하도록 하는 것이 일반적이다.
Vec의 경우처럼 size_type 을 인자로 받는 경우에는 요소의 개수라는 구조를 결정하기 때문에 explicit 이 적당하다.

1.5. 12.5 Conversion operators

클래스의 제작자들은 명시적으로 변환 연산자(conversion operator)를 정의하여 그 객체의 타입이 목적인 객체의 타입으로 변환하는 것을 정하는 것이 가능하다.
변환 연산자의 모양은 operator 목적이 되는 타입의 이름으로 정의된다.
~cpp 
class Student_info {
public:
	operator double();
	// ...
}

...
vector<Student_info> vs;
...
double d = 0;
d += vs[i];		// 이와 같이 형변환을 할 수 있다.

이런 자동 형변환이 이용된 표준 라이브러리로는 cin 들 수 있다.
~cpp 
if (cin>>x) { /* ... */ }

cin>>x
if (cin) { /* ... */ }
이경우 istream 은 void *를 리턴하게 된다.
그런데 istream 클래스는 istream::operator void*()를 정의하여 만약 입력에 문제가 있으면 void* 형으로 0을 그렇지 않은 경우에는 void* 를 리턴하게 함으로써 마치 bool 형처럼 사용하는 것이 가능하다.
void*로 리턴값을 정함으로써 bool 로 정했을 때 나타나는 자동형 변환(정수형으로의) 문제점을 해결할 수 있다.

void* 타입의 포인터는 어떤 객체라도 가리킬 수 있는 포인터 이므로 유니버셜 포인터(universal point)라고 부른다. 그러나 그러나 포인털르 역참조하는 것은 불가능하다. 그러나 bool 타입으로의 변환은 가능하다.

1.6. 12.6 Conversions and memory management

char*, const char* 형으로의 변환
~cpp 
class Str{
public:
	operator char* ();
	operator const char* () const;
	// 이전과 동일
private:
	Vec<char> data;
};

Str s;
ifstream in(s);
하단의 코드가 올바르게 동작하지 못한다. 변환되는 형이 요구되는 형과 전혀 맞지 않기 때문이다.
그렇다고해서 data가 가리키는 포인터를 바로 넘기면 프로그램에서 그 포인터를 통해서 데이터의 수정을 할 수 잇기 때문에 캡슐화의 장점이 사라진다.
이 것을 해결할 방법은 data의 복사본을 만들어서 그 것을 리턴하고 사용이 끝난 뒤에는 복사본을 제거하는 방식으로 해결이 가능하다.
그러나 사용자는 암묵적 변환사이에서 소멸 시켜야할 포인터를 찾을 수 없다.

표준 string 의 경우에는 3가지 종료의 char* 형으로의 변환을 제공하는데
c_str() string 내부의 char*를 리턴한다. 따라서 사용자가 delete를 할 수는 없지만 포인터를 얻어서 수정할 수 있다.
data() c_str()과 비슷하지만 '\0'로 종료되는 문자열이 아닌 배열을 리턴한다.
copy(char* ) 인자로 받은 char*의 공간에 내부의 문제들을 복사해 넣는다. char*공간은 프로그램가 할당하고 해제하는 공간이다.

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