- 값에 의한 전달보다는 상수 객체 참조자에 의한 전달을 사용해라. 대체적으로 효율적이고 복사손실을 막을 수 있다.
- 기본제공 타입 및 STL반복자, 함수 객체 타입에는 값에 의한 전달이 더 적절하다.
값에 의한 전달보다는 상수객체 참조자에 의한 전달
C++은 함수로부터 객체를 전달받거나 함수에 객체를 전달할 때 값에 의한 전달 방식을 사용한다.
특별히 다른 방식을 지정하지 않는 한, 함수 매개변수는 실제 인자의 사본을 통해 초기화되며, 어떤 함수를 호출한 쪽은 그 함수가 반환한 값의 사본을 돌려받는다. 하지만, 값에 의한 전달은 비용이 비싸다.
class Person {
public:
Person();
virtual ~Person();
...
private:
std::string name;
std::string address;
};
class Student: public Person {
public:
Student();
~Student();
...
private:
std::string schoolName;
std::string schoolAddress;
};
위와 같은 클래스가 있다고 가정했을 때, Student를 인자로 전달받고 유효한지 확인하는 함수가 있다고 해보자.
bool validateStudent(Student s);
Student plato;
bool platoIsOk = validateStudent(plato);
plato로부터 매개변수 s를 초기화시키기 위해 Student의 복사 생성자가 호출된다.
그리고 s는 validateStudent가 복귀할 때 소멸자가 호출된다.
또한, Student 객체에는 String 객체가 두 개나 멤버로 들어가 있어 String도 생성해야 하고 Person에서 파생된 클래스이기 때문에 Person 객체도 먼저 생성되어야 한다.
정리하자면, Student 객체를 생성하면 여섯 번에 생성자와 소멸자가 호출된다.
이러한 비용은 상수객체에 대한 참조자로 전달하면 아낄 수 있다.
bool validateStudent(const Student& s);
상수객체에 대한 참조자를 사용하면 새로 만들어지는 객체가 없기 때문에 효율적인 코드가 된다.
여기서 주의해야 할 점은 매개변수에 있는 const이다. validateStudent함수는 전달받은 객체의 유효성만 검증해야 하기 때문에 전달받은 객체에 대한 변화를 보호해야 한다. 값에 의한 전달 방식에서는 객체의 사본을 전달받기 때문에 원본 객체에 대한 보호를 할 수 있었지만 참조자의 경우에는 실제 객체에 대한 참조자를 전달받는 것이기 때문에 const로 보호해 주어야 한다.
또한, 참조에 의한 전달 방식으로 매개변수를 넘기면 복사손실 문제가 없어진다.
파생 클래스 객체가 기본 클래스 객체로서 전달되는 경우에 이 객체가 값에 의한 전달이 이루어지면 기본 클래스의 복사 생성자가 호출되고, 파생 클래스 객체로 동작하게 해주는 특징들이 잘려나간다.
즉, 파생 클래스 객체가 기본 클래스 객체로 취급된다.
class Window {
public:
...
std::string name() const;
virtual void display() const;
};
class WindowWithScrrollBars : public Window {
public:
...
virtual void display() const;
};
이 Window 클래스로 만들어지는 객체는 이름을 갖고 있고 name 멤버 함수로 얻어낸다.
display 멤버 함수로 화면 표시도 가능하다. display방식은 원하는 방식에 맞춰 구현할 수 있게 가상 함수로 되어 있다.
이러한 객체들을 가지고 화면 출력 관련 함수를 만들었다고 가정해보자.
void printNameAndDisplay(Window w)
{
std::cout << w.name();
w.display();
}
이 함수에 WindowWithScrollBars 객체를 넘긴다면 복사손실이 발생한다.
매개변수 w가 생성되기는 하는데, Window 객체로 만들어져 WindowWithScrollBars 객체의 정보는 잘려나간다.
특히, display 함수를 호출하는 부분에서는 Window::display가 호출될 것이다.
상수객체 참조자로 전달하게 되면 복사손실을 해결할 수 있다.
void printNameAndDisplay(const Window& w)
{
std::cout << w.name();
w.display();
}
상수 객체 참조자에 의한 전달 시 주의 사항
참조자는 보통 포인터를 써서 구현된다.
즉, 참조자를 전달하는 것은 결국 포인터를 전달하는 것과 같다.
전달하는 객체의 타입이 기본제공 타입일 경우에는 참조자로 넘기는 것보다 값으로 넘기는 편이 효율이 좋을 때가 있다.
STL의 반복자와 함수 객체에도 마찬가지이다.
반복자와 함수 객체를 구현할 때는 복사 효율을 높일 것과 복사손실 문제에 노출되지 않도록 만드는 것이 필수이다.
기본제공 타입은 작다. 타입 크기가 작다고 값에 의한 전달을 해도 되는 것은 아니다.
객체의 복사 생성자 호출이 저비용일 때 값에 의한 전달을 사용할 근거가 될 수 있는데 멤버가 포인터 하나라고 해도 그 포인터 멤버가 가리키는 대상까지 복사하는 작업도 포함되어 크기가 작지 않을 수 있다.
또한, 크기뿐만 아니라 수행 성능 문제가 있다. 컴파일러 중에는 기본제공 타입과 사용자 정의 타입을 아예 다르게 취급하는 것들이 있다. 예를 들어, 기본제공 타입 double은 레지스터에 넣어주지만 double하나만 포함하는 객체는 레지스터에 넣지 않는다. 이런 컴파일러 환경에서는 차라리 참조에 의한 전달을 쓰는 편이 좋다.
크기가 작다고 해서 작은 사용자 정의 타입을 무조건 값으로 전달할 수 없는 이유가 하나 더 있다.
사용자 정의 타입의 크기는 언제든 변화할 수 있기 때문이다. 지금은 크기가 작더라도 나중에 커질 수 있다.
정리하자면, 일반적으로 값에 의한 전달은 기본제공 타입, STL 반복자, 함수 객체 타입에 사용하고 나머지의 경우에는 참조에 의한 전달을 사용하는 것이 좋다.