Notice
Recent Posts
Recent Comments
Link
«   2026/05   »
1 2
3 4 5 6 7 8 9
10 11 12 13 14 15 16
17 18 19 20 21 22 23
24 25 26 27 28 29 30
31
Archives
Today
Total
관리 메뉴

로또

20. '값에 의한 전달'보다는 '상수객체 참조자에 의한 전달' 방식을 택하는 편이 대개 낫다. 본문

책/Effective C++

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

아롱로또 2023. 11. 17. 14:50

다루는 내용

값에 의한 전달에서 발생하는 오버헤드를 알아보고, 상수객체 참조자 전달의 장점을 알아보자.

 

 

값 전달 vs 상수객체 참조자 전달

기본적으로 C++는 함수로부터 객체를 전달받거나 함수에 객체를 전달할 때, 값에 의한 전달(pass by value) 방식을 사용한다.

#include <iostream>
using namespace std;

class Person{
public:
    Person(){cout << "Person Ctor" << endl;}
    virtual ~Person(){cout << "Person Dtor" << endl;}
private:
    string name;
    string address;
};

class Student: public Person{
public:
    Student(){cout << "Student Ctor" << endl;}
    Student(const Student& rhs){cout << "Student Copy Ctor" << endl;}
    ~Student(){cout << "Student Dtor" << endl;}
private:
    string school_name;
    string school_address;
};

bool ValidateStudent(Student s){
    return true;
}

int main(){
   Student plato;
   bool is_plato_student = ValidateStudent(plato);
   return 0;
}

실행 결과

 

Student 변수 plato를 생성하기 위해 생성자가 호출되고, ValidateStudent 함수에 plato 변수를 전달하면서 복사 생성자가 호출된다. 또한 해당 함수를 종료하면서도 소멸자가 호출되는 모습을 볼 수 있다. 또한 눈에 보이지는 않지만, 각 클래스가 갖는 멤버 변수(name, address)도 생성과 소멸 과정을 동일하게 거칠 것이다.

 

대신, 다음과 같이 함수의 매개 변수를 수정하여 값에 의한 전달이 아니라 상수객체에 대한 참조자 전달로 변경해보자.

bool ValidateStudent(const Student& s){
    return true;
}

실행 결과

 

단지 매개변수만 수정했을 뿐인데 훨씬 효율적인 모습으로 변경된 것을 볼 수 있다. const 키워드를 사용하는 이유는 함수로 전달된 객체를 보호하기 위해서이다. 값에 의한 전달로 함수를 호출하는 경우에 해당 함수에서 전달받은 객체를 수정하더라도 함수를 호출한 곳에서의 객체의 실제 값은 변경되지 않는 것처럼 말이다.

 

복사 손실 문제(slicing problem)

파생 클래스 객체가 기본 클래스 객체로서 전달되는 경우는 드물지 않게 접할 수 있는데, 이 때 이 객체가 값으로 전달되면 기본 클래스의 복사 생성자가 호출되고, 파생 클래스 객체로 동작하게 해 주는 특징들이 "싹둑 잘려" 떨어진다. 기본 클래스 생성자가 만들었으므로 당연한 결과이지만 우리는 분명 이러한 결과를 원치 않을 것이다.

 

class Window{
public:
    string name() const; // 윈도우의 이름 반환
    virtual void display() const; // 윈도우 테두리 및 내부를 그림
};

class WindowWithScrollBars: public Windows{
public:
    virtual void display const;
};

void PrintNameAndDisplay(Window w){ // 복사손실 문제 발생
    cout << w.name() << endl;
    w.display(); // virtual 함수임에도, Window::display 가 호출될 것임
}

int main(){
    WindowWithScrollBars wwsb;
    PrintNameAndDisplay(wwsb); // WindowWithScrollBars 객체를 변수로 넘김
}

PrintNameAndDisplay 함수는 Window 객체를 값에 의해 전달받기 때문에, 실제로 전달하는 객체가 WindowWithScrollBars 이고, virtual keyword를 사용해 작성한 함수일지라도 항상 Window 객체의 display 함수를 호출할 것이다. 대신 상수객체 참조자를 인자로 받도록 수정하자.

void PrintNameAndDisplay(const Window& w){ // 상수객체 참조자
    cout << w.name() << endl;
    w.display();
}

 

값에 의한 전달이 효율적인 경우

참조자는 보통 포인터를 사용해서 구현된다. 즉, 참조자를 전달하는 것은 포인터를 전달하는 것과 일맥상통하다.

때문에 전달하는 객체의 타입이 int 등과 같은 기본제공 타입일 경우에는 참조자로 넘기는 것보다 값으로 넘기는 편이 더 효율적일 때가 많다.

이러한 점은 STL의 반복자와 함수 객체에도 마찬가지이다. 이전부터 반복자와 함수 객체는 값으로 전달되도록 설계해왔다. 또한 반복자와 함수 객체를 구현할 때는 반드시 복사 효율을 높일 것과 복사손실 문제에 노출되지 않도록 만들어야 한다.

 

기본제공 타입의 크기가 단순히 작기 때문에, 크기가 작은 사용자 정의 타입도 값에 의한 전달이 효율적이라고 생각할 수 있다. 하지만 그렇지 않다. 데이터 멤버가 포인터 하나뿐인 객체가 많이 존재하지만 이러한 객체를 복사하기 위해서는 해당 포인터 멤버가 가리키는 대상까지 복사하여야 한다.

복사 생성자 비용이 작다 하더라도 또다른 문제가 있다. 컴파일러 중에는 기본제공 타입과 사용자 정의 타입을 아예 다르게 취급하는 것이 존재한다. 기본제공 타입과 사용자 정의 타입의 하부 표현 구조가 동일하더라도 말이다. 기본제공 타입 double은 레지스터에 넣어주지만 double 하나로만 만들어진 사용자 정의 타입은 레지스터에 넣지 않기도 한다.

또한, 사용자 정의 타입의 크기는 언제든 변화에 노출되어 있다. 지금은 크기가 작을지 몰라도 추후 크기가 커질 가능성이 항상 존재한다.

 

일반적으로 값에 의한 전달이 저비용이라고 가정해도 괜찮은 유일한 타입은 기본제공 타입, STL 반복자, 함수 객체 타입 총 세 가지뿐이다. 이 외의 타입에 대해서는 상수객체 참조자에 의한 전달을 따르는 것이 좋다.

 

이것만은 잊지 말자!

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