관리 메뉴

기억을 위한 기록들

[CPP-effective] 3-3 자원 관리 클래스에서 관리되는 자원은 외부에서 접근가능하게 하자. 본문

C & CPP/Effective C++

[CPP-effective] 3-3 자원 관리 클래스에서 관리되는 자원은 외부에서 접근가능하게 하자.

에드윈H 2021. 4. 8. 15:04

 

이전에 팩토리함수와 같이 예를 들어 함수를 호출한 결과(포인터)를 담기 위해 스마트 포인터를 사용할 수 있었다.

class Player {
public:
	//...
    int GetHP() const
	{
		return 1;
	}
};

Player* CreatePlayer()
{
	return new Player;
};

int main()
{
	std::shared_ptr<Player> player01(CreatePlayer());

	return 0;
}

 

 

여기서 이제 스마트포인터로 만든 player01 객체를 사용하는 전역 함수가 생겼다고 치자.

 

int GetPlayerHP(const Player* p)
{
	return p->GetHP();
}

위에서는 hp정보를 예로 들고,

int main()
{
	shared_ptr<Player> player01(CreatePlayer());

	GetPlayerHP(player01); //컴파일 에러
	return 0;
}

컴파일 에러가 발생한다. hp정보는 보지 못하는 것일까...?

해당 함수는 Player* 인 실제 포인터를 원하는데 나는 공유포인터(shared_ptr)타입의 객체를 넘기고 있다.

 

RAII 클래스(여기선 shared_ptr)의 객체를 실제 원시 포인터(Player*)를 변환할 방법이 필요해지는데, 그 방법으로 일반적인 방법 2가지가 있다.

 

1. 명시적 변환

스마트 포인터에서 명시적 변환을 수행하는 get() 멤버 함수를 제공한다. 이 멤버 함수를 사용하면 각 타입으로 만든 스마트 포인터 객체에 들어 있는 원시 포인터(실제 포인터의 사본)를 얻어낼 수 있다.

int main()
{
	shared_ptr<Player> player01(CreatePlayer());

	int curHP = GetPlayerHP(player01.get()); //성공!

	return 0;
}

 

2. 암시적 변환

 

RAII 객체 안에 들어 있는 실제 자원을 얻어낼 필요가 종종 생기기 때문에, RAII클래스 설계자 중에는 암시적 변환 함수를 제공하여 자원 접근을 매끄럽게 할 수 있도록 만드는 분도 있다.

 

Player클래스 외에 Weapon이라는 클래스를 추가로 만들어서 Player의 무기 정보를 가지게 하고 있도록 만들어 보자.

 

class Weapon
{
public:
	Weapon()
		:damage(5)
	{}

	int GetPower()const
	{
		return damage;
	}
private:
	int damage;
};

그리고 전역 함수로 팩토리 함수를 만들고, 

Weapon CreateWeapon() //간단한 팩토리 함수?
{	
	Weapon newWeapon;
	return newWeapon;
}

 

 

 

Player클래스에서 스마트 포인터로 무기 클래스(Weapon)를 갖고 있게 하고, 암시적 변환을 보기 전에 명시적 변환 함수(GetWeapon())를 넣어 준다.

class Player { 
public:
	//...
	Player()
	{};
	explicit Player(Weapon rhs)
		: weaponInfo(rhs)
	{}

	Weapon GetWeapon() const { //명시적 변환 함수
		return weaponInfo;
	}
private:
	Weapon weaponInfo; //무기정보 변수
};

 그리고 결과를 확인해보면

 

int main()
{	
	Player p1(CreateWeapon());	

	return 0;
}

 

대미지 가 5가 들어가는 Player 객체 p1가 만들어진다. 그리고 만약 플레이어의 무기 정보를 가져올 함수가 필요해졌다 하고, 아래와 같은 함수를 만들어준다. (Weapon의 GetPower() 멤버 함수는 public이라 그냥 호출되긴 하는데 여기선 그냥 보여주기용)

int GetPlayerPower(Weapon p)
{
	return p.GetPower();
}

그러면 이제 Player클래스 안에 명시적 변환해준 함수(GetWeapon)를 호출해주면 소유하고 있는 weapon의 정보를  문제없이 가져온다.

int main()
{	
	Player p1(CreateWeapon());	

	int playerPower = GetPlayerPower(p1.GetWeapon());

	return 0;
}


하지만 변환할 때마다 함수를 호출하지 않고 암시적 변환 함수를 Player에 넣어줄 수 있다.

 

class Player { 
public:
	//...
	Player()
	{};
	explicit Player(Weapon rhs)
		: weaponInfo(rhs)
	{}

	Weapon GetWeapon() const { 
		return weaponInfo;
	}

	operator Weapon() const { //암시적 변환 함수 추가
		return weaponInfo;
	}
private:
	Weapon weaponInfo;
};

 

이렇게 암시적 변환 함수를 추가해주게 되면, GetWeapon() 멤버 함수 호출 없이 Weapon의 대미지 정보를 가져올 수 있다.

int main()
{	
	Player p1(CreateWeapon());	

	int playerPower = GetPlayerPower(p1); //GetWeapon()함수 호출을 하지 않았다.

	return 0;
}

 그렇다고 무조건 좋은 것만은 아니다. 

암시적 변환이 들어가면 실수를 저지를 여지가 많아진다.   

 

int main()
{	
	Player p1(CreateWeapon());	

	int playerPower = GetPlayerPower(p1);

	Weapon w1 = p1; //??? Player객체를 복사하려고 했는데, p1이 Weapon으로
	                //바뀌고 복사가 되어버렸다.
	return 0;
}

 

RAII 클래스를 실제 자원으로 바꾸는 방법으로서 명시적 변환을 제공할지 아니면 암시적 변환을 허용할지에 대한 결정은 그 RAII클래스만의 특정한 용도와 사용 환경에 따라 달라집니다.

 

늘 그런 건 아니지만 암시적 변환보다 get을 사용하는 명시적 변환 함수를 제공하는 쪽이 나을 때가 많습니다. 하지만 암시적 타입 변환에서 생기는 사용 시의 자연스러움이 빛을 발하는 경우도 있다는 점이 있다는 것을 알아두셨으면 합니다.

 

 

 

정리

* 실제 자원을 직접 접근해야 하는 기존 API들도 많기 때문에, RAII클래스를 만들 때는 그 클래스가 관리하는 자원을 얻을 수 있는 방법을 열어 주어야 합니다.

* 자원 접근은 명시적 변환 혹은 암시적 변환을 통해 가능하다. 안정성만 따지면 명시적 변환이 대체적으로 더 낫지만, 고객 편의성(?)을 놓고 보면 암시적 변환도 괜찮다.