프로그래밍 공부
작성일
2024. 2. 7. 16:09
작성자
WDmil
728x90

8.4 중첩된 클래스

클래스 정의가 메서드나 데이터 멤버만 담을 수 있는 것은 아니다. 클래스 정의에는 struct, typedef, enum도 포함될 수 있다. 클래스 안에 선언된 것들은 클래스명 에 스코프 지정 연산자::를 붙여서 이용 가능하다. 단, pulbic으로 선언되어 있어야 한다.

 

클래스 안에 또 다른 클래스를 정의하는 것도 가능하다. 예를 들어 SpreadSheetCell 클래스가 SpreadSheet 클래스의 구성 요소로 완전하게 포함된다면 다음과 같이 두 클래스를 동시에 정의할 수 있다.

class SpreadSheet
{
	class SpreadSheetCell
	{
	public:
		SpreadSheetCell();
		SpreadSheetCell(double initalValue);
		SpreadSheetCell(const std::string& initalValue);

	public:
		void set(double inValue);
		void set(const std::string& inString);

		double getValue() const;
		const std::string& getString() const;

	private:
		std::string doubleToString(double inValue) const;
		double stringToDouble(const std::string& inString) const;

	private:
		double mValue;
		std::string mString;
		mutable int mNumAccesses = 0;
	};
};

SPreadSheetCell 클래스가 SpreadSheet클래스 안에 중첩되어 정의되었기 떄문에 이 클래스를 이용하기 위해서는 스코프 지정 연산자 SpreadSheet::를 붙여야 한다. 스코프 지정 연산자는 메서드를 정의할 떄도 마찬가지로 적용된다. 예를 들어 SpreadSheetCell클래스의 디폴트 생성자는 다음과 같이 정의할 수 있다.

 

SpreadSheet::SpreadSheetCell::SpreadSheetCell() : mValue(0)
{
}

이러한 문법은 코드를 지저분하게 만든다. 예를 들어 SpreadSheetCell의 대입 연산자는 다음처럼 정의해야 한다.

SpreadSheet::SpreadSheetCell& SpreadSheet::SpreadSheetCell::operator=(const SpreadSheetCell& rhs)
{
    if (this == &rhs) {
        return *this;
    }
    mValue = rhs.mValue;
    mString = rhs.mString;
    mNumAccesses = rhs.mNumAccesses;
    return *this;
}

SpreadSheet클래스에 속한 메서드라도 리턴타입에는 스코프 지정 연산자를 이용해야 한다.

단, 파라미터에는 적용하지 않아도 된다.

SpreadSheet::SpreadSheetCell& SpreadSheet::getCellAt(int x, int y)
{
    if (!inRange(x, mWidth) || !inRange(y, mHeight)) {
        throw std::out_of_range("");
    }
    return mCells[x][y];
}

스코프 지정 연산자 떄문에 코드가 지저분해지는 것을 피하려면 다음과 같이 타입 에일리어스를 이용해서 SpreadSheet::SpreadSheetCell을 SCell로 축약해서 사용한다.

using SCell = SpreadSheet::SpreadSheetCell;

타입 에일리어스 구문은 반드시 SpreadSheet클래스 정의 바깥에 있어야 한다. 그렇지 않으면 타입 에일리어스된 이름조차도 스코프 지정 연산자 SpreadSheet::을 붙여서 SpreadSheet::SCell로 사용해야 한다!

 

이 타입 에일리어스 를 이용하면 아래처럼 생성자 정의를 간결하게 할 수 있다.

SCell::SpreadSheetCell() : mValue(0)
{
}

중첩된 클래스도 일반적인 접근 제어를 그대로 적용받는다. 만약 중첩된 클래스를 private나 protected 블록에서 선언했다면 클래스 밖에서는 이용할 수 없다.

 

단순한 클래스에 대해서만 중첩된 클래스로 이용하는 것이 바람직하다. SpreadSheetCell 클래스는 중첩된 클래스로 이용하기에는 너무 코드가 지저분해진다.


8.5 클래스 종속 열거 데이터 타입

상수값을 정의해야 한다면 #define 대신 열거타입을 이용하는 것이 좋다. 예를 들어 각 셀이 가질 수 있는 색상을 설정하기 위해 SpreadSheetCell 클래스를 다음처럼 정의할 수 있다.

class SpreadSheetCell
{
public:
	enum class Colors { Red = 1, Green, Blue, Yellow };
	void setColor(Colors color);
private:
	Colors mColor = Colors::Red;
};

setColor() 메서드의 정의는 다음과 같다.

void SpreadSheetCell::setColor(Colors color)
{
    mColor = color;
}

이 메서드는 열거타입의 컬러 상수값을 이용하여 다음과 같이 호출할 수 있다.

	SpreadSheetCell myCell(5);
	myCell.setColor(SpreadSheetCell::Colors::Blue);

열거 타입을 이용하는 것이 #define 상수를 이용하는 것보다 더 바람직하다. #define 상수를 이용하면 클래스 정의가 다음처럼 된다.

#define SPREADSHEETCELL_RED 1
#define SPREADSHEETCELL_GREEN 2
#define SPREADSHEETCELL_BLUE 3
#define SPREADSHEETCELL_YELLOW 4
class SpreadSheetCell
{....

#define 상수를 이용하면 색상 변수의 타입을 int로 밖에 할 수 없다. 그러면 setColor()함수의 파라미터도 int타입이 되어야 하고 색상으로 정의되지 않은 상수값이 설정될 가능성이 있어, 이에대한 검사를 런타임에 별도로 해야한다.

 

반면, Colors와 같은 열거 타입을 이용하면 컴파일러가 빌드 타임에 잘못된 상수의 이용을 검사해주어 더 효율적이고 안전하다.


8.6 friend속성

friend설정을 이용하면 다른 클래스 또는 다른 클래스의 메서드에서 private나 protected로 선언된 멤버와 메서드에 접근할 수 있게 열어줄 수 있다. 예를 들어 SpreadSheetCell 클래스는 SpreadSheet 클래스를 다음처럼 friend로 선언할 수 있다.

class SpreadSheetCell
{
public:
	friend class SpreadSheet;
...

이렇게 선언하면 SpreadSheet의 모든 메서드는 SpraedSheetCell의 private와 protected 멤버/메서드 에 자유롭게 접근할 수 있다.

 

SpreadSheet의 특정 메서드만 접근 권한을 열어주고 싶다면 다음과 같이 선언한다.

	friend void SpreadSheet::setCellAt(int x, int y,
		const SpreadSheetCell& cell);

friend선언은 권한을 열어줄 클래스에서만 할 수 있다. 즉, 다른 클래스에 대해 접근 권한을 요청하는 클래스 또는 메서드, 함수 스스로 다른 클래스의 friend선언을 할 수는 없다.

 

friend의 활용 예로, SpreadSheetCell의 문자열이 공백이 아닌지 검증하는 함수를 클래스 외부에 독립적으로 두고 싶을 수 있다.

 

문자열을 검증하기 위해서는 SpreadSheetCell의 내부 데이터에 접근할 수 있어야 하므로 해당 검증 함수 checkSpreadSheetCell()을 다음과 같이 SpreadSheetCell 클래스 정의에서 friend로 선언한다.

class SpreadSheetCell
{
public:
	friend bool checkSpreadSheetCell(const SpreadSheetCell& cell);
    ...

클래스 내에서의 friend선언은 함수 프로토타입 선언으로 취급되기 때문에 다른 곳에 별도로 해당 함수의 프로토타입을 선언해줄 필요가 없다.

bool checkSpreadSheetCell(const SpreadSheetCell& cell)
{
    return !(cell.mString.empty());
}

friend 함수는 일반 함수와 동일하게 정의하면 된다. 단, friend로 허락받은 클래스의 private와 protected 멤버/메서드에 접근 가능하다는 점만 다르다. 함수를 정의할 때는 friend키워드를 사용하지 않아도 된다.

 

friend는 남용되기 쉽다. friend를 이용하면 내부 구조를 다른 클래스나 함수에 노출시키기 때문에 추상화 원칙이 훼손된다.

 

그러므로 연산자 오버로딩과 같이 불가피한 상황에서만 이용하는 것이 바람직하다. 연산자 오버로딩을 할 떄는 private와 protected멤버에 접근할 수 있어야 한다.


8.7 연산자 오버로딩

객체를 대상으로 덧셈 또는 비교하거나 객체 간 또는 파일로부터 스트리밍을 해야 할 때가 종종 있다. 예를 들어 스프레드 시트는 각 셀에 대해 합계와 같은 산술 연산을 할 수 있어야 의미가 있다.


8.7.1 예제: SpreadSheetCell에 덧셈 기능 구현

객체지향 방식이라면 SpreadSheetCell 객체 간에 서로 덧셈이 가능해야 한다. 두 셀을 더하면 새로운 셀이 결괏값이 된다. 이러한 덧셈은 원본 셀을 변경하지 않는다. SpreadSheetCell에 대한 덧셈은 그 셀이 가진 값에 대한 덧셈을 의미한다.


8.7.1.1 첫 번째 시도 : add메서드

SpreadSheetCell에 다음처럼 add()메서드를 선언한다.

class SPreadSheetCell
{
    public:
    SpreadSheetCell add(const SpreadSheetCell& cell) const;
}

이 메서드는 두 셀의 값을 더한 결과를 값으로 가지는 새로운 셀을 리턴한다. 이 메서드는 덧셈이 행해질 두 원본 셀에 아무런 변경도 가하지 않기 떄문에 cosnt메서드로 선언되고, 파라미터인 두 번째 셀로 const타입으로 받는다. 다음은, add()메서드의 구현 예 이다.

SpreadSheetCell SpreadSheetCell::add(const SpreadSheetCell& cell) const
{
    SpreadSheetCell newCell;
    newCell.set(mValue + cell.mValue); // mValue와 mString 멤버를 업데이트 한다.
    return newCell;
}

위 메서드에서는 새로운 SpreadSheetCell 객체 newCell을 생성한다. newCell은 값에 의한 전달로 리턴된다. 즉, 메서드를 호출한 쪽에서 다시 한 번 새로운 객체를 만들어 newCell을 복제하여 이용한다. 값에 의한 전달 대신 복제가 일어나지 않는 참조형으로 리턴하면 어떨까 하고 생각할 수도 있다.

 

하지만 이 메서드가 리턴하는 순간 newCell 객체는 소멸되기 떄문에 참조형 으로 리턴하게 되면 메서드의 호출이 끝나자마자 댕글링참조(dangling reference)가 되어버린다.

 

add() 메서드를 다음과 같이 이용할 수 있다.

	SpreadSheetCell myCell(4), anotherCell(5);
	SpreadSheetCell aThirdCell = myCell.add(anotherCell);

add() 메서드도 쓸만하다. 하지만 조금 부자연스럽다는 것을 알 수 있다.


8.7.1.2 두 번째 시도: operator+ 의 오버로딩

두 셀을 두 개의 int와 두 개의 double타입처럼 + 기호를 이용해서 더할 수 있다면 편리할 것이다. 즉, 다음과 같은 형태로 이용할 수 있다면 훨씬 간결하다.

	SpreadSheetCell aThirdCell = myCell + anotherCell;

C++는 프로그래머가 자신만의 + 기호를 정의할 수 있도록 지원한다. 이를 덧셈 연산자라 하며 내가 작성한 클래스와 연동하도록 커스터마이즈 할 수 있다. 이를 위해 먼저 operator+를 클래스 정의에서 선언한다.

class SPreadSheetCell
{
	public:
    //코드생략
	SpreadSheetCell operator+(const SpreadSheetCell& cell) const;
    // 코드 생략
};

operator+에 대한 구현부 정의는 add()메서드와 같다.

SpreadSheetCell SpreadSheetCell::operator+(const SpreadSheetCell& cell) const
{
    SpreadSheetCell newCell;
    newCell.set(mValue + cell.mValue);
    return newCell;
}

 

이제 두 셀 객체를 + 기호를 이용해서 더할 수 있다.

 

이 문법에 익숙해지려면 약간의 시간이 필요하다. operator+라는 이름이 낯설게 보일 수 있다. 하지만, 이 이름은 add처럼 일반적인 메서드 이름과 다를 바 없다. 나머지 문법을 이해하려면 내부적으로 무슨 일이 일어나는지 알아야 한다.

 

C++ 컴파일러가 코드를 파싱하다가 +, -, =, << 와 같은 기호를 만나게 되면 각각 operator+, operator-, operator=, operator<<를 찾아서 파라미터 타입에 적합한 메서드를 호출하게 된다.

 

예를 들어 컴파일러가 다음과 같은 라인을 만나면 SpreadSheetCell 클래스나 전역 함수 중에 두 개의 SpreadSheetCell 타입 파라미터를 받는 operator+라는 이름의 메서드나 함수가 있는지 찾는다.

SpreadSheeCell aThirdCell = myCell + anotherCell;

만약, SpreadSheetCell 에 operaotr+메서드가 구현되어 있다면, 위 코드를 아래와 같은 메서드 호출 형태로 고쳐쓸 수 있다.

SpreadSheetCell aThirdCell = myCell.operator+(anotherCell);

 

operator+메서드에서 받는 파라미터 두 개가 항상 같은 데이터 타입일 필요는 없다. 예를 들어 SpreadSheetCell 클래스에서 SpreadSheet 클래스를 파라미터로 받는 operator+메서드를 정의할 수도 있다. 물론 논리적으로 말이 안되지만 컴파일러에는 아무런 문제가 없다.

 

파라미터 타입 뿐만 아니라 리턴 타입도 자유롭게 선택할 수 있다. 연산자 오버로딩은 함수 오버로딩과 같다. 함수 오버로딩이 그렇듯이 연산자 오버로딩도 리턴 타입에 대해서는 상관하지 않는다.


묵시적인 타입 캐스팅

이전 예제와 같이 operator+메서드를 정의해두면 놀랍게도 셀 뿐만 아니라 string, double, int타입 데이터도 + 기호를 이용해서 덧셈을 할 수 있게 된다!

SpreadSheetCell myCell(4), aThirdCell;
strign str = "Hello";
aThirdCell = myCell + str;
aThirdCell = myCell + 5.6;
aThirdCell = myCell + 4;

그 이유는 컴파일러가 operator+를 처리할 때 적합한 타입만 찾는 것이 아니라 적합한 타입으로 변환할 방법이 있는지도 찾기 때문이다.

 

클래스 생성자는 이러한 변환에서 중요한 역할을 한다. 만약 생성자 중에 부적합한 타입을 인자로 받는 것이 있다면 그    생성자를 이용해서 적합한 타입의 임시 객체를 자동으로 생성한다. 예를 들어 위 코드에서 SpreadSheetCell에 double 값을 더하려 할 때 컴파일러는 SpreadSheetCell에 double 타입 파라미터를 받는 생성자가 있는 것을 발견하고 SPreadSheetCell 의 임시객체를 만들어서 operator+를 실행한다.

 

string도 마찬가지로 SpreadSheetCell에 string 타입 파라미터를 받는 생성자를 이용해서 임시 객체를 만들어 operator+를 실행한다.

 

이러한 암묵적인 변환은 프로그래머의 편의를 위한 것이다. 하지만 위 예에서 SpreadSheetCell에 string을 더하는 것은 그리 논리적이지 않다. 이러한 암묵적인 변환을 막고 싶다면 해당 생성자에 explicit키워드를 붙여준다.

	explicit SpreadSheetCell(const std::string& initalValue);

explicit 키워드는 클래스 정의에서만 사용할 수 있고 파라미터가 하나뿐인 생성자에서만 의미가 있다. 여기에는 파라미터가 여러 개지만 디폴트 값이 지정되어 있어서 결과적으로 파라미터 하나만 넘겨도 되는 생성자도 포함된다.

 

묵시적 생성자가 실행된다는 것은 임시 객체가 생성되었다가 없어진다는 것 이기 때문에 비효율적이다. 다음과 같이 operator+매서드를 double타입에 대해 추가로 오버로딩하면 임시 객체 생성을 줄일 수 있다.

SpreadSheetCell SpreadSheetCell::operator+(double rhs) const
{
    return SpreadSheetCell(mValue + rhs);
}

위 코드에서는 SpreadSheetCell 생성자를 바로 리턴하고 있다. 객체를 리턴하기 위해 변수를 선언할 필요는 없다.


8.7.1.3 세 번째 시도: 전역 함수로서의 operator+

묵시적인 변환은 SpreadSheetCell 객체에 int나 double타입 데이터를 더할 수 있게 해주었다. 하지만 다음과 같이 덧셈 연산임에도 교환법칙이 성립되지 않는다.

aThirdCell = myCell + 4;	// 정상 작동
aThirdCell = myCell + 5.6;	// 정상 작동
aThirdCell = 4 + myCell;	// 컴파일 실패!
aThirdCell = 5.6 + myCell;	// 컴파일 실패!

묵시적인 변환은 연산자의 좌항이 SpreadSheetCell 객체인 경우에만 작동하고 우향에 위치하면 작동하지 않는다.

 

덧셈은 인자의 좌우 위치와 관계없어야 하므로 분명 무언가 잘못되었다. 여기서 문제는 operator+를 SpreadSheetCell의 메서드로 정의하면 객체가 우향에 있을 떄만 operator+가 호출된다는 데 있다. C++에서 객체의 메서드가 호출되는 방식이 그렇게 디자인 되었기 때문이다. 이 상태에서는 위 문제 코드가 정상 작동할 방법이 없다.

 

하지만 operator+를 전역 함수로 정의하여 특정 객체에 종속되지 않도록 하면 문제가 해결된다. 다음과 같이 전역함수 operator+를 정의한다.

friend SpreadSheetCell operator+(const SpreadSheetCell& lhs, const SpreadSheetCell& rhs);
SpreadSheetCell operator+(const SpreadSheetCell& lhs, const SpreadSheetCell& rhs)
{
    SpreadSheetCell newCell;
    newCell.set(lhs.mValue + rhs.mValue);
    return newCell;
}

이제 모든 라인이 기대했던 대로 작동한다.

 

그런데, 만약, 다음과 같은 연산을 할 경우 어떻게 될까?

aThirdCell = 4.5 + 5.5;

이럴 경우 위 코드는 문제없이 컴파일되고 실행도 된다. 그런데, operator+함수가 호출되지 않는다.

 

위 코드는 4.5와 5.5에 대해 보통의 double타입 덧셈을 먼저 수행하여 결과적으로 다음과 같은 코드가 된다.

aThirdCell = 10;

위 대입 코드가 작동하기 위해서는 SpreadSheetCell 객체가 우항에 있어야 한다. 컴파일러는 double 타입 파라미터를 받는 생성자를 발견하고 묵시적인 변환을 수행하야 SpreadSheetCell의 임시 객체를 생성한 다음 대입연산자를 호출한다.


8.7.2 산술 연산자 오버로딩

지금까지 operator+ 메서드와 함수를 어떻게 작성할 수 있는지 알아보았다. 나머지 산술 연산자의 오버로딩도 비슷한 방법으로 구현할 수 있다. 다음은 -, *, / 연산자에 대한 선언이다.

 

(% 연산자도 오버로딩할 수 있지만, SpreadSheetCell의 double값에 적용하기에는 논리적으로 불합리하다.)

	friend SpreadSheetCell operator+(const SpreadSheetCell& lhs, const SpreadSheetCell& rhs);
	friend SpreadSheetCell operator-(const SpreadSheetCell& lhs, const SpreadSheetCell& rhs);
	friend SpreadSheetCell operator*(const SpreadSheetCell& lhs, const SpreadSheetCell& rhs);
	friend SpreadSheetCell operator/(const SpreadSheetCell& lhs, const SpreadSheetCell& rhs);
SpreadSheetCell operator+(const SpreadSheetCell& lhs, const SpreadSheetCell& rhs)
{
    SpreadSheetCell newCell;
    newCell.set(lhs.mValue + rhs.mValue);
    return newCell;
}

SpreadSheetCell operator-(const SpreadSheetCell& lhs, const SpreadSheetCell& rhs)
{
    SpreadSheetCell newCell;
    newCell.set(lhs.mValue - rhs.mValue);
    return newCell;
}

SpreadSheetCell operator*(const SpreadSheetCell& lhs, const SpreadSheetCell& rhs)
{
    SpreadSheetCell newCell;
    newCell.set(lhs.mValue * rhs.mValue);
    return newCell;
}

SpreadSheetCell operator/(const SpreadSheetCell& lhs, const SpreadSheetCell& rhs)
{
    SpreadSheetCell newCell;
    newCell.set(lhs.mValue / rhs.mValue);
    return newCell;
}

곱셈 연산자에서 나눗셈을 하고 덧셈 연산자에서 뺄셈을 하더라도 아무런 강제사항이 없다. C++에서는 오버로딩된 연산자가 일반 상식에 어긋나는지 어떤지 검사하지 않는다. 하지만 특별한 이유가 없다면 각 연산 기호가 가진 일반적인 의미에서 그 기능을 쉽게 유추할 수 있도록 구현하는 것이 바람직하다.


8.7.2.1 축약형 산술 연산자의 오버로딩

기본 산술 연산자 뿐만 아니라 +=, -=과 같은 축약형 산술 연산자도 오버로딩 할 수 있다. 어쩌면 operator+를 구현하는것 만으로도 +=도 자동으로 지원될 것이라 짐작했을지도 모른다.

 

안타깝게도 전혀 그렇지 않다. 축약형 연산자도 명시적으로 오버로딩 해야한다. 축약형 연산자는 기본 연산자와 두가지 면에서 다르다. 

 

첫 번째 차이점은, 임시 객체를 만들지 않고 좌항 객체를 변경한다는 점이다.

두 번째는 좀더 미묘한 차이점으로, 대입 연산자가 그랬던 것처럼 변형된 객체에 참조형을 결과 리턴값으로 생성한다는 것이다.

 

축약형 산술 연산자는 객체의 메서드로서, 좌항에 항상 객체가 위치해야 한다.

	SpreadSheetCell& operator+=(const SpreadSheetCell& rhs);
	SpreadSheetCell& operator-=(const SpreadSheetCell& rhs);
	SpreadSheetCell& operator*=(const SpreadSheetCell& rhs);
	SpreadSheetCell& operator/=(const SpreadSheetCell& rhs);
SpreadSheetCell& SpreadSheetCell::operator+=(const SpreadSheetCell& rhs)
{
    set(mValue + rhs.mValue);
    return *this;
}

SpreadSheetCell& SpreadSheetCell::operator-=(const SpreadSheetCell& rhs)
{
    set(mValue - rhs.mValue);
    return *this;
}

SpreadSheetCell& SpreadSheetCell::operator*=(const SpreadSheetCell& rhs)
{
    set(mValue * rhs.mValue);
    return *this;
}

SpreadSheetCell& SpreadSheetCell::operator/=(const SpreadSheetCell& rhs)
{
    set(mValue / rhs.mValue);
    return *this;
}

축약형 산술 연산자는 기본 산술 연산자와 대입 연산자를 조합한 것이다. 위와 같이 연산자 오버로딩을 해두면 다음 과 같이 이용할 수 있다.

	aThirdCell += myCell;
	aThirdCell += 5.4;

	5.4 ++aThirdCell; // 컴파일 실패! 좌항이 객체여야한다!

8.7.3 비교 연산자 오버로딩

>, <, == 와 같은 비교 연산자도 객체 간 작업에 활용하기 좋은 연산자다. 기본산술 연산자에서 와 마찬가지로 이들 연산자도 전역 friend함수로 만들어서 좌항과 우항 어느쪽 값이든 묵시적인 변환을 이용할 수 있도록 한다. 이때 클래스의 내부 멤버에 접근 가능하도록 클래스에서 friend로 선언해준다. 비교 연산자는 모두 bool타입 결괏값을 리턴하는 것이 바람직하다.

 

물론 아무 타입이나 리턴할 수 있지만 bool타입이 일반적인 관례에 가장 적합하다.

	friend bool operator==(const SpreadSheetCell& lhs,
		const SpreadSheetCell& rhs);
	friend bool operator<(const SpreadSheetCell& lhs,
		const SpreadSheetCell& rhs);
	friend bool operator>(const SpreadSheetCell& lhs,
		const SpreadSheetCell& rhs);
	friend bool operator!=(const SpreadSheetCell& lhs,
		const SpreadSheetCell& rhs);
	friend bool operator<=(const SpreadSheetCell& lhs,
		const SpreadSheetCell& rhs);
	friend bool operator>=(const SpreadSheetCell& lhs,
		const SpreadSheetCell& rhs);
bool operator==(const SpreadSheetCell& lhs, const SpreadSheetCell& rhs)
{
    return (lhs.mValue == rhs.mValue);
}

bool operator<(const SpreadSheetCell& lhs, const SpreadSheetCell& rhs)
{
    return (lhs.mValue < rhs.mValue);
}

bool operator>(const SpreadSheetCell& lhs, const SpreadSheetCell& rhs)
{
    return (lhs.mValue > rhs.mValue);
}

bool operator!=(const SpreadSheetCell& lhs, const SpreadSheetCell& rhs)
{
    return (lhs.mValue != rhs.mValue);
}

bool operator<=(const SpreadSheetCell& lhs, const SpreadSheetCell& rhs)
{
    return (lhs.mValue <= rhs.mValue);
}

bool operator>=(const SpreadSheetCell& lhs, const SpreadSheetCell& rhs)
{
    return (lhs.mValue >= rhs.mValue);
}

 

위 비교 연산자들은 double타입 멤버 mValue를 이용하고 있다. 일반적으로 부동소수점 변수간에 동일 값인지 테스트하는 것은 오버헤드가 크다 이떄는 엡실론 값 비교 를 하는것이 좋다.

 

데이터 멤버 개수가 많은 클래스라면 객체 간 비교를 구현하기가 상당히 성가시다. 하지만 ==, < 두 비교 연산자만 구현해놓으면 다른 연산자는 이들 연산자를 활용하여 간편하게 구현할 수 있다. 예를 들어 operator>=은 operator<를 이용해서 다음과 같이 구현할 수 있다.

bool operator>(const SpreadSheetCell& lhs, const SpreadSheetCell& rhs)
{
    return !(lhs < rhs);
}

 

이러한 연산자들은 객체를 int또는 double값과 비교하는데도 사용할 수 있다.


8.7.4 연산자 오버로딩이 포함된 데이터 타입 정의

많은 사람들이 처음에는 연산자 오버로딩 문법이 어렵고 혼란스럽다고 느낀다. 하지만 연산자 오버로딩은 코드를 간단하게 만들려는 목적으로 디자인되었다. 그런데 이상의 설명에서 눈치챘듯, 클래스를 사용하는 입장에서는 더 간단해지지만 클래스를 만들어야 하는 입장에서는 오히려 더 복잡해진다.

 

연산자 오버로딩을 단순하게 하려면 클래스를 최대한 int와 double같은 기본 타입과 비슷한 개념으로 디자인하는 것이 좋다.

 

당연하지만 add()나 sum()같은 메서드를 기억하여 이용하는 것보다 + 기호를 이용하는 것이 훨씬 간단하다.

 

정확히 어떤 연산자를 오버로딩할 수 있는지 궁금할 수 있는데, 이에대한 답은 '거의 모든 연산자' 로 평소 들어보지도 못했던 연산자도 포함한다.

 

스트림삽입 과 추출 연산자도 오버로딩하면 매우 유용하다. 처음에는 생각하기 어렵지만, 까다로우면서도 재미있는 연산자 오버로딩도 있다. STL에서는 연산자 오버로딩을 전반적으로 활용하고 있다.

728x90