본문 바로가기
Object Oriented Programming(C++)/Effective C++

Effective C++ | 항목 20 값에 의한 전달보다는 상수 객체 참조자에 의한 전달이 대개 낫다

by continue96 2022. 2. 3.

항목 20 값에 의한 전달보다 상수 객체 참조자에 의한 전달이 대개 낫다

20.1 값에 의한 전달: 객체 편

 기본적으로 C++는 함수에 객체를 전달하거나 함수로부터 객체를 전달받을 때 값에 의한 전달(pass by value) 방식을 사용한다. 특별히 다른 방식을 지정하지 않는 한, 함수 매개 변수는 실제 인자의 사본을 통해 초기화되고 함수를 호출한 쪽은 그 함수가 반환한 값의 사본을 돌려받는다. 바로 이 사본을 만들어내는 행위 때문에 값에 의한 전달은 고비용의 연산이 된다.

 아래 코드를 한번 보자. validateStudent 함수는 Student 인자를 값으로 전달받고 이 인자가 유효한지를 알려주는 bool 값을 반환한다. 이 함수가 호출될 때 과연 어떤 일이 일어날까?

class Person {
public:
	Person();
	virtual ~Person();
private:
	std::string name;
	std::string address;
};

class Student : public Person {
public:
	Student();
	virtual ~Student();
private:
	std::string schoolName;
	std::string schoolAddress;
};

bool validateStudent(Student s); /* Student 객체를 값으로 전달합니다. */

Student plato;
bool platoIsOK = validateStudent(plato); /* 생성자 여섯 번, 소멸자 여섯 번 호출됩니다. */

 

 단지 Student 객체 하나를 값으로 전달했을 뿐인데 Student 복사 생성자 호출 한 번, Person 복사 생성자 호출 한 번, String 복사 생성자 호출 네 번이 일어난다. Student 객체의 사본이 소멸될 때도 앞서 호출된 생성자와 대응되어 소멸자가 호출된다. Student 객체를 값으로 전달하기 위해 날아간 비용이 생성자 6번, 소멸자 6번이나 된다.

 

20.2 상수 참조자에 의한 전달: 객체 편

 생성자와 소멸자 호출을 몇 번씩 거치지 않고 넘어가려면 상수 객체에 대한 참조자(pass by reference to const)로 전달한다.

bool validateStudent(const Student& s); /* Student 객체를 상수 참조자로 전달합니다. */

 

20.2.1 상수 객체 참조자와 효율성

 상수 객체 참조자를 전달하면 객체가 새로 만들어지지 않기 때문에 생성자와 소멸자가 호출되는 일이 없어 훨씬 효율적인 코드로 바뀐다. 여기서 새겨둬야 할 부분은 매개 변수 선언문에 있는 const이다. 매개 변수에 const 키워드를 붙여 validateStudent 함수로 전달된 Student 객체가 변하지 않도록 해야 한다.

 

20.2.2 상수 객체 참조자와 슬라이스 문제

 참조에 의한 전달 방식으로 매개 변수를 넘기면 슬라이스 문제(slicing problem)가 없어지는 장점도 있다. 파생 클래스 객체가 기본 클래스 객체로서 전달되는 경우가 있는데, 이때 이 객체가 값으로 전달되면 기본 클래스의 복사 생성자로 객체가 생성되어 파생 클래스의 특징들이 싹둑 잘려나가게 된다. 예를 들어, 그래픽 기반의 윈도우 시스템을 구현한 클래스 라이브러리로 어떤 작업을 하고 있다고 가정해보자.

class Window {
public:
	std::string name() const; /* 윈도우의 이름을 반환합니다. */
	virtual void display() const; /* 윈도우의 테두리와 내부를 출력합니다. */
};

class WindowWithScrollBars : public Window {
public:
	virtual void display() const; /* 윈도우의 테두리, 내부, 그리고 스크롤 바를 출력합니다. */
};

void printNameAndDisplay(Window w) { /* Window 객체를 값으로 전달합니다. */
	std::cout << w.name();
	w.display();
}

WindowWithScrollBars wwsb;
printNameAndDisplay(wwsb); /* 매개 변수가 슬라이스 문제에 부딪힙니다. */

 

 틀리게 구현한 printNameAndDisplay 함수에 WindowWithScrollBars 객체를 넘기면 매개 변수 w가 생성되긴 하는데 Window 객체로 만들어지면서 wwsb가 WindowWithScrollBars 객체의 구실을 할 수 있는 정보가 전부 날아간다. w는 Window 클래스 객체의 면모만을 갖고 있으므로 안에서 호출되는 display는 Window::display일 것이다.

 

 슬라이스 문제를 피하려면 w를 상수 객체에 의한 참조자를 전달하도록 만들면 된다. 그러면 w는 어떤 종류의 윈도우가 넘겨지더라도 그 윈도우의 성질을 온전히 갖게 된다.

void printNameAndDisplay(const Window& w) { /* Window 객체를 상수 참조자로 전달합니다. */
	std::cout << w.name();
	w.display();
}

 

20.3 값에 의한 전달: 기본 타입, STL 반복자, 함수 객체 편

 전달하는 객체의 타입이 기본제공 타입(int, double 등), STL 반복자, 함수 객체일 경우에는 참조자로 넘기는 것보다 값으로 넘기는 편이 더 효율적이다. 참고로, 반복자와 함수 객체를 구현할 때는 반드시 복사 효율을 높이고 슬라이스 문제가 발생하지 않도록 만드는 것이 필수다.

 

20.4 상수 객체 참조 의한 전달: 사용자 정의 타입 편

 값에 의한 전달 방식을 주로 사용하는 기본제공 타입에 착안하여 사용자가 정의한 타입의 크기가 작으면 전부 값에 의한 전달을 할 수 있다고 생각하는 사람이 있다. 이는 잘못된 생각으로, 객체의 크기가 작다고 해서 그 객체의 복사 생성자 호출도 저비용일 것이라 여기면 안 된다. 게다가 객체의 크기가 작고 복사 생성자도 그다지 비싸지 않게 만들어졌다고 해도 기본제공 타입과 사용자 정의 타입을 아예 다르게 취급하는 컴파일러가 있다. 이런 개발 환경에서는 참조에 의한 전달을 쓰는 편이 낫다.

 

NOTE
① 값에 의한 전달보다는 상수 객체 참조자에 의한 전달을 선호하자. 대체적으로 효율적일 뿐만 아니라 슬라이스 문제까지 막아준다.
② 기본제공 타입, STL 반복자, 그리고 함수 객체 타입만큼은 값에 의한 전달이 더 적절하다.

댓글