본문 바로가기

Information Technology/C++

[C++] 객체잘림과 reference_wrapper

이 글은 개인의 학습을 목적으로 정리한 글입니다. 이점 참고하고 읽어주세요 ;)


이번 포스팅에서는 클래스 상속과 관련해서 객체 잘림이라는 현상과 reference_wrapper에 대해 알아보겠습니다.

클래스의 상속 구조에서 자식 클래스는 부모 클래스보다 많은 멤버를 가질 수 있습니다. 함수의 경우 오버 라이딩을 통해 부모 클래스의 함수를 자식 클래스에 맞추어 변경할 수도 있고요.

#include <iostream>
#include <vector>
#include <functional>

using namespace std;

class Base
{
public:
	int m_i = 0;

	virtual void print()	// 자식 클래스에서의 다형성을 위해 virtual 선언
	{
		cout << "I'm Base" << endl;
	}
};

class Derived : public Base
{
public:
	int m_j = 1;

	virtual void print() override	// 부모 클래스의 print 함수를 자식 클래스에서 오버라이딩
	{
		cout << "I'm Derived" << endl;
	}
};

위의 예제 코드 역시 부모 클래스인 Base 클래스에서 print 함수를 virtual 선언을 하고 있습니다. 이를 통해 Base 클래스를 상속받는 Derived 클래스는 print 함수를 재정의하고 있습니다.

void doSomething(Base& b)
{
	b.print();
}

int main()
{
	Derived d;
	Base& b = d;
    	b.print(); // virtual 선언을 해줬기 때문에 "I'm Derived" 출력

	return 0;
}

그리고 전역 함수로 Base 클래스의 인스턴스를 레퍼런스로 받아와 print 함수를 호출하는 doSomething 함수를 만들고,

main 함수에서는 Derived 클래스의 인스턴스 d를 정의하고 Base 클래스 인스턴스의 레퍼런스인 b에 d를 저장했습니다.

그리고 b의 멤버인 print 함수를 호출하면, print 함수는 virtual 선언이 되어있기 때문에 다형성을 통해 자식 클래스인 Derived 클래스의 print 함수가 호출됩니다.

int main()
{
	Derived d;
	Base b = d;
	b.print(); // 객체 잘림이 발생하여 Base 클래스의 print 함수 호출
	return 0;
}

그런데 만약 Base 클래스의 인스턴스를 레퍼런스가 아닌 그냥 인스턴스로 받으면 어떻게 될까요?

Base b = d; 선언은 마치, Base 클래스 인스턴스 b를 정의한 후에 b에 d를 복사하는 것과 같기 때문에 다형성이 상실된 채로 d가 b에 복사됩니다. 때문에 b의 print 함수를 호출하면 Derived 클래스의 print 함수가 아닌 Base 함수의 print 함수가 호출됩니다. 뿐만 아니라 Derived 클래스가 가지고 있는 다른 멤버들의 값 역시 b에 저장되지 못하고 사라집니다.

이렇게 부모 클래스가 자식 클래스의 멤버들을 다 받지 못하는 것을 객체 잘림 현상이라고 합니다.

int main()
{
	Base b;
	Derived d;

	std::vector<Base> my_vec;
	my_vec.push_back(b);
	my_vec.push_back(d);

	for (auto& e : my_vec)
		e.print();
}

위의 예제에서도 역시, my_vec 벡터가 Base 클래스 자료형이기 때문에

for-each문을 통해 my_vec에 저장된 멤버들의 print 함수를 호출시켜보면 객체 잘림 현상으로 인해 Derived 클래스의 인스턴스인 d 역시 부모 클래스인 Base 클래스의 print 함수를 호출하는 걸 볼 수 있습니다.

int main()
{
	Base b;
	Derived d;

	std::vector<Base*> my_vec;	// 포인터를 사용
	my_vec.push_back(&b);
	my_vec.push_back(&d);

	for (auto& e : my_vec)
		e->print();
}

이를 해결하는 방법은 위의 코드 블록처럼 벡터의 자료형을 Base 클래스의 포인터로 설정하는 것입니다. 벡터는 자료형으로 레퍼런스(&)를 받지 않기 때문에 포인터를 사용하면 이번에는 다형성이 상실되지 않아 Derived 클래스의 인스턴스 d가 자신이 속한 클래스의 print 함수를 호출하는 걸 알 수 있습니다.

 

위처럼 포인터를 사용하지 않고 객체 잘림 현상을 해결할 수 있는 방법이 있는데, 그게 바로 reference_wrapper입니다.

#include <functional>

int main()
{
	Base b;
	Derived d;

	std::vector<std::reference_wrapper<Base>> my_vec;
	my_vec.push_back(b);
	my_vec.push_back(d);

	for (auto& e : my_vec)
		e.get().print(); // get()을 꼭 사용
}

reference_wrapper는 <functional> 라이브러리에 포함된 기능이기 때문에 헤더에서 꼭 <functional> 헤더를 추가해줘야 합니다.

사용법은 위의 코드 블록과 같습니다. vector에서 자료형을 선언할 때, 사용하려는 클래스를 std::reference_wrapper의 bracket <>으로 묶어주면 됩니다.

그리고 인스턴스의 멤버를 호출할 때는 멤버 뒤에. get()을 선언하고 호출하려는 멤버를 사용하면 됩니다.

vector를 사용하는 게 아닌 상황이라면 편하게 레퍼런스&를 사용하면 되겠지만, vector는 referece를 자료형으로 받지 않기 때문에 이런 상황에는 reference_wrapper를 사용하면 될 것 같습니다.