Item 20: ‘값에 의한 전달’보다는 ‘상수객체 참조자에 의한 전달’ 방식을 택하는 편이 대개 낫다

기본적으로 C++는 함수로부터 객체를 전달받거나 함수에 객체를 전달할 때 ‘값에 의한 전달(pass-by-value)’을 사용한다.(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;
};
bool validateStudent(Student s);

Student plato;
bool platoIsOK = validateStudent(plato);

이 함수의 호출 순서를 정리하면 다음과 같다.

  • plato로부터 매개변수 s를 초기화시키기 위해 Student의 복사 생성자가 호출된다.
    • 추가로 s는 validateStudent가 복귀할 때 소멸된다.
  • Student 객체에 존재하는 string 객체도 생성자가 호출된다.
  • Person객체로 파생되었기 때문에 string 객체가 두 개 더 생성된다.
  • 앞 서 호출된 생성자에 대응되는 소멸자가 호출된다.

객체 하나를 값에 의한 전달 했지만 실행된 연산은 생성자 6번과 소멸자 6번이다.

상수 참조자에 의한 전달

생성자 호출을 몇번씩 사용하지 않고 해결할 수 있는 방법이 상수객체에 대한 참조자(const reference)를 사용하는 것이다.

bool validateStudent(const Student& s);

이렇게 하는 순간 코드는 훨씬 더 효율적으로 동작한다. 여기서 중요한 점은 const로 기존 함수는 매개변수를 값으로 받도록 되어 있기 때문에, 호출부에서는 함수로 전달된 Student 객체에 어떤 변화가 생기더라도 그 변화로부터 안전하게 보호받는 점을 알고 있다.

변경된 함수는 객체의 전달 방식이 참조에 의한 전달이기 때문에 const를 붙여 객체의 내용을 변경하지 못하도록 한다.

또한 참조에 의한 전달 방식으로 매개변수를 넘기면 복사손실 문제가 없어지는 장점도 있다. 파생 클래스 객체가 기본 클래스 객체로서 전달되는 경우는 드물지 않게 접할 수 있는데 이때 이 객체가 값으로 전달되면 기본 클래스의 복사 생성자가 호출되고, 파생 클래스 객체로 동작하게 해 주는 특징들이 복사되지 않는다.

C++ 참조자는 보통 포인터를 써서 구현되기에 참조자를 전달한다는 것은 결국 포인터를 전달한다는 것과 일맥상통한다.

값에 의한 전달 vs 상수 참조자에 의한 전달

위에서 말한 레퍼런스는 결국 포인터를 써서 구현된다는 맥락하에 객체의 기본 타입이 기본 제공 타입일 경우에는 참조자로 넘기는 것보다 값으로 넘기는 편이 더 효율적일 때가 많다. 그러니 ‘값에 의한 전달’ 및 ‘상수객체의 참조에 의한 전달’ 중 하나를 선택해야 할 때, 기본 제공 타입에 대해서는 ‘값에 의한 전달’을 선택하더라도 이유가 있다는 것이다.

이 점은 STL의 반복자와 함수 객체에 대해서도 마찬가지다. 예전부터 반복자와 함수객체는 값으로 전달되도록 설계해 왔기 때문이다. 참고로, 반복자와 함수 객체를 구현할 때는 반드시 복사 효율을 높일 것과 복사분실 문제에 노출되지 않도록 만드는 것이 필수다. (Item 1의 내용으로 다중 패러다임 언어의 특징이자 C++이 어려운 이유)

크기가 작은 사용자 정의 타입이라면?

기본제공 타입이 작기 때문에 이를 연결하여 사용자 정의 타입이 작다면 문제가 없다고 생각할 수 있다. 크기가 작으니 복사 생성의 비용이 저렴하다는 뜻은 아니다. 실제로 반복에서 위험한 것이지.. 포인터 멤버가 가리키는 대상까지 복사하는 작업도 따라다녀야 한다.

그렇다면 복사 생성의 비용도 비싸지 않다고 가정한다고 한다면? 이번엔 수행 성능 문제가 발생할 수 있다. 추가로 사용자 정의 타입은 언제든지 크기가 변화할 수 있다는 가능성이 존재하기 때문에 더더욱 문제가 된다.

정리

C++에서 정말 중요한 파트이자 아마 면접 단골 문제가 아닐까 싶다. 얇게 아는 것보다 이에 대한 깊이와 경험을 가지는 것도 좋을 것 같다.

물론 RVO의 동작이 있기 때문에 모든 경우에 적용되는 것은 아니지만, 대개 상수 참조자에 의한 전달이 더 낫다.

  • ‘값에 의한 전달’ 보다는 ‘상수객체 참조자에 의한 전달’ 방식을 택하는 편이 대개 낫다.
  • 이번 항목에서 다룬 법칙은 기본 제공 타입 및 STL 반복자, 그리고 함수 객체 타입에는 맞지 않는다. 이에 대해서는 값에 의한 전달이 더 적합하다.

태그: ,

카테고리:

업데이트:

댓글남기기