Effective C++/Chapter 4: 설계 및 선언

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

yeoul0714 2025. 5. 29. 02:25

1. 개요


C++에서는 함수로부터 객체를 받거나 함수에 객체를 전달할때 값에 의한 전달(pass-by-value)를 사용하기도 합니다.

 

특별한 경우가 아니면 함수의 매개변수는 실제 인자의 '사본'을 받게 됩니다.

 

그리고 호출한 쪽은 '사본'을 돌려받습니다.

 

여기서 이러한 사본을 만들어 내는 주체는 복사 생성자 입니다.

 

바로 이러한 점 때문에 값에 의한 전달의 비용이 매우 커지기도 합니다.

 

이번 항목은 이러한 주제를 바탕으로 참조자에 의한 전달을 권장하는 이유를 설명하는 항목입니다.


 

2. 값에 의한 전달이 비싼 이유


class Person {
public:
    Person(); // 간결함을 위해 매개변수 생략
    virtual ~Person(); // virtual인 이유는 항목 7 확인
 
private:
    std::string name;
    std::string address;
};

class Student: public Person {
public:
    Student(); 
    virtual ~Student();

private:
    std::string schoolName;
    std::string schoolAddress;
};

 

우선 이러한 예시 코드가 있습니다.

 

소멸자가 virtual인 이유는 항목7을 참고해주세요.

https://yeoul0714.tistory.com/33

 

[Effective C++] 항목 7: 다형성을 가진 기본 클래스에서는 소멸자를 반드시 가상 소멸자로 선언하자

1. 부모 클래스 소멸자를 virtual로 해야하는 이유우리는 기본 클래스의 소멸자는 반드시 virtual로 선언해주어야 합니다. 아래 예시를 통해서 그 이유를 알아보도록 합시다.// Effective C++ 항목 7: 다

yeoul0714.tistory.com

 

 

그리고 아래와 같이 함수를 호출한다고 가정해보겠습니다.

bool validateStudent(Student s); //Student 객체를 값으로 전달 받는 함수
Student plato; // 소크라테스 제자 플라톤
bool platoIsOK = validateStudent(plato);

 

이 상황에서 과연 얼마나 많은 객체가 생성될까요? 

 

지금부터 한번 면밀하게 알아보도록 하겠습니다.

 

1. plato 객체로 부터 매개변수 s를 초기화 하기 위해서 Student의 복사 생성자 호출

 

2. validateStudent함수가 return될때 Student 소멸자 호출

 

이렇게 두단계로 끝난다면 정말 좋겠지만 사실 과정은 좀더 복잡합니다.

 

Student 객체는 string 멤버변수를 2개나 가지고 있습니다.

 

그래서 Student의 객체가 생기면 이 두객체도 함께 생성되어야 합니다.

 

또한 Student는 Person의 파생클래스이기에 Person객체도 Student객체가 생기기 전에 생성되야 합니다.

 

그리고 Person객체도 string 멤버변수를 가지고 있어서 함께 생성되어야 합니다.

 

결론적으로 Student 복사 생성자 호출 1회, Person 복사 생성자 호출 1회, string 복사 생성자 호출 4회가 일어나게 됩니다.

 

여기서 끝이 아닙니다. 

 

소멸될때도 이렇게 생성된 객체들의 소멸자가 전부 호출되게 됩니다.

 

객체를 값으로 전달하게 되면 결국 생성자 호출 6회, 소멸자 호출 6회가 일어나게 됩니다.


 

3. 이러한 문제 해결하는 법


이러한 문제를 해결하는 방법이 존재합니다.

 

그것은 바로 상수객체에 대한 참조자로 객체를 전달하면 됩니다.

bool validateStudent(const Student& s);

 

이렇게 하면 그렇게 많이 호출되던 생성자, 소멸자가 전혀 호출되지 않습니다.

 

그러나 신경써줘야 할 부분은 const입니다.

 

값에 의한 전달을 할때엔 사본이 넘어가기 때문에 원본 객체가 변경될 걱정을 하지 않아도 되지만

 

참조자로 전달을 할때는 const가 붙지 않으면 넘겨준 객체가 변경되지 않을까 하는 걱정을 하게 되기 마련입니다.

 

결론은 이렇게 참조자로 전달을 받게 되면 함수내부에서 전달 받은 객체가 변경될 가능성이 있기 때문에  const를 이용해서 보호해줍니다.


 

4. 참조자로 전달시 장점


3번에서 언급한 생성자, 소멸자가 호출되지 않는 장점외에도 한가지 장점이 더 존재합니다.

 

그것은 바로 복사 손실 문제를 해결한다는 것입니다.

 

복사 손실문제는 파생 클래스 객체를 기반 클래스 타입으로 값 전달할 때, 파생 클래스의 고유 부분이 잘려나가는 현상입니다.

 

파생 클래스의 특징들이 값으로 전달하게 되면 전부 사라지게 됩니다. (기본 클래스 생성자가 호출되기에 당연합니다.)

 

아래와 같은 클래스가 있다고 가정해 보겠습니다.

 

display함수는 파생 클래스에서 상속을 받고 있고 파생클래스 만의 구현이 있을 것입니다.

class Window {
public:
    std::string name() const; // return name of window
    virtual void display() const; // draw window and contents
};

class WindowWithScrollBars: public Window {
public:
    virtual void display() const;
};

 

만약 이러한 함수가 있다고 할때 이 함수의 매개변수로 WindowWithScrollBars객체를 넘기면 어떻게 될까요?

void printNameAndDisplay(Window w) 
{ 
    std::cout << w.name();
    w.display();
}

 

저 함수 안에서는 Window객체가 만들어지기는 하지만 파생 클래스의 정보가 전부 잘려나가게 됩니다.

 

파생 클래스의 display는 절대로 호출될 수 없습니다.

 

아래처럼 참조로 매개변수를 받게 되면 전혀 문제가 없이 파생클래스의 내용도 가지게 됩니다.

void printNameAndDisplay(const Window& w) 
{
    std::cout << w.name();
    w.display();
}

 

5. 항상 참조로 값을 넘겨주어야 하나?


5-1. 기본타입은 값으로 넘겨주자

 

사실 항상 참조로 값을 넘겨주는것이 정답은 아닙니다.

 

참조자는 사실상 포인터를 전달하는 의미와 일맥상통합니다.

 

int와 같은 기본타입은 오히려 값으로 전달하는 편이 좀더 효율적일때가 많습니다.

 

그래서 항상 참조로 값을 넘겨주는 것이 정답을 아니라는 것을 기억하시길 바랍니다.

 

이는 iterator와 함수 객체도 마찬가지 입니다. (값으로 넘겨주는 다른 예시)

 

참고로 iterator는 구현시에 1. 복사효율을 높이고 2. 복사손실 문제에 노출되지 않게 하는 것이 필수입니다.

 

 

5-2. 크기가 작다고 복사비용까지 작은건 아니다.

int같은 타입은 4바이트입니다.

 

그런데 참조로 값을 넘기게 된다면 오히려 포인터의 크기인 8바이트를 넘겨주게 되어서 원본보다 크기가 커지는 문제가 생깁니다.

 

이러한 예시를 보고 크기가 작으면 무조건 값에 의한 전달로 해야한다고 오해할지도 모릅니다.

 

그러나 크기가 작다고 복사비용까지 적을것이라고 생각하는 것은 큰 오해입니다.

 

만약 문자열을 복사한다고 생각해본다면 문자열의 첫번째 주소를 가르키는 포인터 변수자체는 작지만

 

완벽한 복사를 위해서는 문자열의 모든 내용을 전부 복사해야합니다.

 

5-3. 크기와 복사비용 다 작으면 괜찮나?

이러한 경우에도 문제가 생길지 모릅니다.

 

컴파일러들 중에는 기본타입과 사용자 정의 타입에 대해서 다르게 처리하는 경우가 있습니다.

 

 double타입은 레지스터에 넣어주지만 double을 하나 가지고 있는 사용자 정의 타입의 객체는 레지스터에 들어가지 않습니다.

 

이러한 경우엔 참조에 의한 전달이 낫습니다.

 

포인터 변수는 항상 레지스터에 들어가기 때문입니다.

 

5-4. 사용자 정의 타입을 항상 값으로 전달하면 안되는 이유

사용자 정의 타입의 경우에는 언제든 그 크기가 변할 가능성이 있습니다.


 

6. 결론


값에 의한 전달이 비용이 적다고 가정해도 좋은 타입들은 다음과 같습니다.

 

1. 기본제공 타입

2. STL Iterator

3. 함수 객체 타입

 

이렇게 3가지뿐이고, 이 외의 타입은 상수객체 참조자에 의한 전달을 하는것이 좋습니다.

 

이것만은 잊지 말자!

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

 

https://yeoul0714.tistory.com/65

 

[Effective C++] 항목 21: 함수에서 객체를 반환해야 할 경우에 참조자를 반환하려고 들지 말자

1. 개요우리는 항목 20에서 값에 의한 전달의 효율 문제에 대해서 공부했습니다. 그래서 우리는 기존 코드에서 값에 의한 전달이 있는 경우에 그것을 참지못하는 경우가 생기게 됩니다. 그러나

yeoul0714.tistory.com