[Effective C++]'값에 의한 전달'보다는 '상수객체 참조자에 의한 전달' 방식을 택하는 편이 대개 낫다

설계 및 선언, 세 번째 이야기

Posted by SungBeom on December 08, 2019 · 5 mins read

값에 의한 전달

기본적으로 C++은 함수로부터 객체를 전달받거나 함수에 객체를 전달할 때 '값에 의한 전달(pass-by-value)' 방식을 사용합니다. 특별히 다른 방식을 지정하지 않는 한, 함수 매개변수는 실제 인자의 '사본'을 통해 초기화되며, 어떤 함수를 호출한 쪽은 그 함수가 반환한 값의 '사본'을 돌려받습니다. 이들 사본을 만들어내는 원천이 바로 복사 생성자인데요, 이 점 때문에 '값에 의한 전달'이 고비용의 연산이 되기도 합니다. 아래의 클래스 계통을 보고 생각해 보도록 합시다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
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;
};
cs

이제 아래의 코드를 봅시다. validateStudent라는 함수를 호출하고 있는데, 이 함수는 Student 인자를 전달받고(값으로) 이 인자가 유효화됐는가를 알려 주는 값을 반환합니다.

1
2
3
4
5
bool validateStudent(Student s);
 
Student plato;
 
bool platoIsOK = validateStudent(plato);
cs

이 함수가 호출될 때 어떤 일이 일어날까요? plato로부터 매개변수 s를 초기화시키기 위해 Student의 복사 생성자가 호출될 것입니다. 게다가 s는 validateStudent가 복귀할 때 소멸될 것이고요. 정리하면, 이 함수의 매개변수 전달 비용은 Student의 복사 생성자 호출 한 번, 그리고 Student의 소멸자 호출 한 번입니다. 여기서 Student 객체에는 String 객체 두 개가 멤버로 들어 있기 때문에, Student 객체가 생성될 때마다 이들 String 객체들도 덩달아 생성되어야 합니다. 게다가 Student 객체는 Person 객체로부터 파생되었기 때문에, Student 객체가 생성되면 Person 객체도 (먼저) 생성되어야 합니다. Person 객체 안에는 또 String 객체가 두 개가 들어 있기 때문에, Person 객체가 매번 생성될 때 String 생성자가 두 번 더 불리게 되겠지요. Student 객체를 값으로 전달하는 데 날아간 비용을 계산해 보니 생성자 여섯 번에 소멸자 여섯 번입니다!

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

위처럼 생성자와 소멸자 호출을 몇 번씩 거치지 않고 넘어갈 수 있는 방법이 있습니다. 상수객체에 대한 참조자(reference-to-const)로 전달하게 만드는 것입니다.

bool validateStudent(const Student& s);
이렇게 하면 순식간에 훨씬 효율적인 코드로 바뀝니다. 새로 만들어지는 객체 같은 것이 없기 때문에, 생성자와 소멸자가 전혀 호출되지 않거든요. 여기서 중요한 부분은 매개변수 선언문에 있는 const입니다. 원래의 validateStudent는 Student 매개변수를 값으로 받도록 되어 있기 때문에, 호출부에서는 함수로 전달된 Student 객체에 어떤 변화가 생기더라도 그 변화로부터 안전하게 보호를 받습니다. validateStudnet가 상대하는 Student 객체는 원본이 아닌 사본이니까요. 그런데 이제는 Student 객체의 전달 방식이 참조에 의한 전달입니다. 매개변수 앞에 const가 붙은 건 바로 그 때문인데, 이것이 붙지 않으면 validateStudent 함수로 넘어간 Student 객체가 변할지도 모른다는 걱정을 호출부가 해야 하거든요.

참조에 의한 전달 방식으로 매개변수를 넘기면 복사손실 문제(slicing problem)가 없어지는 장점도 있습니다. 파생 클래스 객체가 기본 클래스 객체로서 전달되는 경우에는, 기본 클래스의 복사 생성자가 호출되고, 파생 클래스 객체로 동작하게 해 주는 특징들이 '싹둑 잘려' 떨어지고 맙니다. 하지만 상수객체에 대한 참조자로 전달하도록 만들면 해당 문제는 해결됩니다.

참조자는 보통 포인터를 써서 구현됩니다. 즉, 참조자를 전달한다는 것은 결국 포인터를 전달한다는 것과 일맥상통한다는 이야기죠. 이렇게 따져 보면, 전달하는 객체의 타입이 개본제공 타입(int 등)일 경우에는 참조자로 넘기는 것보다 값으로 넘기는 편이 더 효울적일 때가 많습니다. 이 점은 STL의 반복자와 함수 객체에도 마찬가지입니다. 예전부터 반복자와 함수 객체는 값으로 전달되도록 설계해 왔기 때문입니다.
참고로, 반복자와 함수 객체를 구현할 때는 반드시 복사 효율을 높일 것과 복사손실 문제에 노출되지 않도록 만드는 것이 필수입니다.

기본제공 타입의 크기가 작아서 '값에 의한 전달'이 효율적인 것은 아닙니다. 일반적으로 '값에 의한 전달'이 저비용이라고 가정해도 괜찮은 유일한 타입은 기본제공 타입, STL 반복자, 함수 객체 타입, 이렇게 세 가지뿐입니다. 이 외의 타입에 대해서는 '값에 의한 전달'보다는 '상수객체 참조자에 의한 전달'이 좋습니다.


정리

'값에 의한 전달'보다는 '상수 객체 참조자에 의한 전달'을 선호합시다. 대체적으로 효율적일 뿐만 아니라 복사손실 문제까지 막아 줍니다.
기본제공 타입 및 STL 반복자, 그리고 함수 객체 타입에는 '값에 의한 전달'이 더 적절합니다.