C++는 기본적으로 함수로부터 객체를 전달받거나 함수에 객체를 전달할 때는 값에 의한 전달방식(pass-by-value)을 사용한다. C에서 물려받은 특성중 하나이다. 특별히 다른 방식을 지정하지 않는 한, 함수 매개변수는 실제 인자의 사본을 통해 초기화 되며, 어떤 함수를 호출한 쪽은 그 함수가 반환한 값의 사본을 돌려받는다.
이 사본을 만들어내는 원천이 복사 생성자이다. 이 점에 의해 값에 의한 전달이 고비용의 연산이 되기도 한다.
class Person
{
public:
Person();
virtual ~Person();
private:
string name;
string address;
};
class Student : public Person
{
public:
Student();
~Student();
private:
string schoolName;
string schoolAddress;
};
bool validateStudent(Student s);
int main()
{
Student plato;
bool plaotIsOK = validateStudent(plato);
return 0;
}
validateStudent라는 함수는 Student 인자를 전달받고 이 인자가 유효화됐는가를 알려 주는 값을 반환한다.
validateStudent가 호출될 떄 어떤 일이 일어날까?
1.Person 복사 생성자
2.Person의 string name 복사 생성자
3.Person의 string address 복사 생성자
4.Student 복사 생성자
5.Student의 string schoolName 복사 생성자
6.Student의 string schoolAddress 복사 생성자
또한 validateStudent 함수가 종료될 때 마다 s의 소멸자도 호출할 것이다.
실행자체에는 문제가 없겠다면 좀 더 가볍게 구현하기 위해선 상수객체에 대한 참조자(reference-to-const)로 전달하게 만드는 것이다.
bool validateStudent(const Student& s);
새로 만들어지는 객체 같은 것이 없기 때문에, 생성자와 소멸자가 전혀 호출되지 않는다.
여기서 주의 깊게 볼건 const이다. 원래의 validateStudent는 Student 매개변수를 값으로 받도록 되어 있기 때문에, 사본이 생성되어 사본에 변화가 생기더라도 그 변화로부터 원본은 안전하게 보호 받는다는 보장이 되어있다. 하지만 참조에 의한 전달은 원본이 넘어가기에 원본이 변화에 대한 보장이 되어있지때문에 const를 붙이는게 좋다.
참조에 의한 전달 방식으로 매개변수를 넘기면 복사손실 문제(slicing problem)(*주 1)가 없어지는 장점도 있다. 파생 클래스 객체가 기본 클래스 객체로서 전달되는 경우가 종종 있는데, 이때 이 객체가 값으로 전달되면 기본 클래스의 복사 생성자가 호출되고, 파생 클래스 객체로 동작하게 해 주는 특징이 잘려나가고 만다. 쉽게 말해서 파생 클래스 이름뿐인 기본 클래스 객체란 소리다.
그래픽 기반의 윈도우 시스템을 구현한 클래스 라이브러리를 써서 어떤 작업을 한다고 가정해보자.
class Window
{
public:
string name() const;
virtual void display() const;
};
class WindowWithScrollBars : public Window
{
public:
virtual void display() const;
};
void printNameAndDisplay(Window w)
{
cout << w.name() << endl;
w.display();
}
int main()
{
WindowWithScrollBars wwsb;
printNameAndDisplay(wwsb);
return 0;
}
printNameAndDisplay 함수에서 매개변수 w가 복사하면서 사본을 생성하는데, Window객체로 만들어 지면서 wwsb가 WindowWithScrollBars 객체의 구실을 할 수 있는 부속 정보가 잘려나간다. printNameAndDisplay 함수 안에서 w는 어떤 타입의 객체가 넘겨지든 Window 클래스 객체의 면모만 갖게 된다. 쉽게 설명하면 Window의 복사 생성자만 호출되고 WindowWithScrollBars의 복사 생성자는 호출되지 않았기 때문이다.
복사손실 문제에 대한 해결법은 상수객체에 대한 참조자로 전달하도록 만들면 된다.
void printNameAndDisplay(const Window& w)
{
cout << w.name() << endl;
w.display();
}
이제 w는 어떤 종류의 Window가 넘겨지더라도 그 Window의 성질을 갖게 된다.
참조자는 보통 포인터를 써서 구현된다는 사실이 있다. 즉, 참조자를 전달한다는 것은 결국 포인터를 전달한다는 것과 같다. 전달하는 객체의 타입이 기본제공 타입(int 등)일 경우에는 참조자로 넘기는 것보다 값으로 넘기는 편이 더 효율적일 때가 많다. 값에 의한 전달 및 상수객체의 참조에 의한 전달 중 하나를 선택해야 할 때, 기본제공 타입에 대해서는 값에 의한 전달을 선택하더라도 괜찮다.
이 점은 STL의 반복자와 함수 객체에도 마찬가지이다. 예전부터 반복자와 함수 객체는 값으로 전달되도록 설계해 왔기 때문이다.
반복자와 함수 객체를 구현할떄는 반드시 생각해야 할 점이 있다.
1.복사 효율을 높일 것
2.복사손실 문제에 노출되지 않도록 만들 것
솔직히 타입의 크기만 작으면 별로 상관없는거 아니야? 라는 생각이 든다. 하지만 그냥 크기가 작다고 그 객체의 복사 생성자 호출이 저비용은 아니다. 데이터 멤버라고 해 봐야 포인터 하나뿐인 객체가 꽤많지만(STL 컨테이너 대부분), 이런 객체를 복사하는 데는 그 포인터 멤버가 가리키는 대상까지 복사하는 작업도 따라다녀야 하기에 크기가 작으면 저비용이란 공식은 성립하지 않다.
크기가 작다고 해서 사용자 정의 타입, 클래스를 무조건 값으로 전달하면 안되는 이유가 하나 더 있다.
사용자 정의 타입의 크기는 언제든 변화에 노출되어 있다라는 점이다. 이 점은 내가 갖고 있는 문제다. 항상 설계했던 것보다 클래스의 크기는 커진다. 일반적으로 값에 의한 전달이 저비용이라고 가정해도 괜찮은 경우는 기본제공 타입, STL 반복자, 함수 객체 타입 이렇게 세가지 뿐이다.
이것만은 잊지 말자
'값에 의한 전달'보다는 '상수 객체 참조자에 의한 전달'을 선호하자. 대체적으로 효율적일 뿐만 아니라 복사손실 문제까지 막아준다.
기본제공 타입 및 STL 반복자, 함수 객체 타입에는 맞지 않는다. 이들에 대해서는 '값에 의한 전달'이 더 적절하다.
(*주 1) 복사손실 문제(slicing problem)
값에 의한 복사를 할때 기본 클래스의 매개변수로 파생클래스의 객체가 전달될 때 사본을 만들기 위해 기본 클래스의 복사 생성자만 호출하여 파생 클래스의 멤버데이터는 복사되지 않아 파생 클래스의 기능을 잃는 상황이 발생한다.
'서적 정리 > Effective C++' 카테고리의 다른 글
24.타입 변환이 모든 매개변수에 대해 적용되어야 한다면 비멤버 함수를 선언하자 (0) | 2021.12.15 |
---|---|
23.멤버 함수보다는 비멤버 비프렌드 함수와 더 가까워지자. (0) | 2021.12.14 |
22.데이터 멤버가 선언될 곳은 private 영역임을 명심하자 (0) | 2021.12.14 |
21.함수에서 객체를 반환해야 할 경우에 참조자를 반환하려고 들지 말자 (0) | 2021.12.14 |
19.클래스 설계는 타입 설계와 똑같이 취급하자 (0) | 2021.12.14 |
18.인터페이스 설계는 제대로 쓰기엔 쉽게, 엉터리로 쓰기엔 어렵게 하자 (0) | 2021.12.14 |
17.new로 생성한 객체를 스마트 포인터에 저장하는 코드는 별도의 한 문장으로 만들자 (0) | 2021.12.11 |
16.new 및 delete를 사용할 때는 형태를 반드시 맞추자 (0) | 2021.12.11 |
댓글