이펙티브 C++
https://www.yes24.com/Product/Goods/17525589
페이지 149p ~ 160p
요약
항목 20: ‘값에 의한 전달’보다는 상수객체 참조자(레퍼런스)에 의한 전달’ 방식을 택하는 편이 대개 낫다
- 기본적으로 C++는 함수로부터 객체를 전달받거나 함수에 객체를 전달할 때 ‘값에 의한 전달’ 방식을 사용한다.(C에서 물려받은 특성 중 하나)
- 특별히 다른 방식을 지정하지 않는 한, 함수 매개변수는 실제 인자의
사본
을 통해 초기화되며, 어떤 함수를 호출한 쪽은 그 함수가 반환한 값의사본
을 리턴받는다. 사본
을 만들어내는 원천이 바로 복사 생성자이다.사본
을 만들 때 복사 생성자를 사용하기 때문에 ‘값에 의한 전달’이 고비용의 연산이 될 수 있다.
- 특별히 다른 방식을 지정하지 않는 한, 함수 매개변수는 실제 인자의
class Person {
public:
Person();
virtual ~Person();
private:
std::string name;
std::string address;
}
class Student: public Person {
public:
Student();
virtual ~Student();
private:
std::string schoolName;
std::string schoolAddress;
}
bool validateStudent(Student s);
Student plato;
bool platoIsOK = validateStudent(plato);
- 위와 같은 상황에서는
plato
로 부터 매개변수 s를 초기화시키기 위해 Student의 복사 생성자가 호출될 것이다.- 그리고 함수가 종료될 때 초기화된 s는 소멸될 것이다.
- 여기에 Student 객체에는 string 객체 멤버가 두 개 존재하기 때문에 string 객체 멤버의 생성자도 함께 호출될 것이며, Student 객체는 Person 객체의 파생 클래스이기 때문에 Person 객체가 먼저 생성되는 일도 발생한다.
- 최종적으로는 Student 객체 하나를 값으로 전달했는데, Student의 복사 생성자가 호출, Person의 복사 생성자가 호출, 그리고 각각의 멤버로 가지고 있는 string의 복사 생성자가 네 번 발생한다.
- 이에 대응되는 소멸자도 각각 호출된다.
- 비효율적인 부분이 있지만, 이 동작은 그래도 정상적으로 작동하기 때문에 값에 의한 전달이 꼭 나쁘다는 생각이 들지 않을 수 있다.
- 하지만 불필요한 생성자의 호출을 아낄 수 있는 방법이 있다.
bool validateStudent(const Student& s);
- 이렇게 하면 새로 만들어지는 객체가 없기 때문에 훨씬 효율적인 코드가 된다.
- 여기에서 중요한 것은
const
키워드가 사용된 부분이다.- 값에 의한 전달은 객체의 사본을 전달하기 때문에, 함수 내부에서 객체를 변화시켜도, 그 변화로부터 원본 객체를 안전하게 지킬 수 있다는 장점이 있다.
- 그러나 레퍼런스는 객체를 직접 참조하기 때문에 함수 내부에서 원본 객체를 변화시킬 가능성이 있는데,
const
키워드를 통해 이를 방지할 수 있다.
복사손실 문제(slicing problem)
- 레퍼런스에 의한 전달 방식을 사용하면 복사손실 문제도 해결할 수 있다.
- 복사손실 문제는 파생 클래스 객체가 기본 클래스 객체로 전달되는 경우 드물지 않게 접할 수 있는 문제이다.
- 파생 클래스 객체가 기본 클래스 객체로 전달되면 기본 클래스의 복사 생성자가 호출되고, 파생 클래스의 부분은 호출되지 않는다.
- 따라서 값에 의해 전달된 파생 클래스 객체는 기본 클래스 객체가 되어 원하는 방식대로 작동하지 않을 가능성이 있다.
기본제공 타입과 사용자 정의 타입
기본제공 타입의 크기는 사용자 정의 타입에 비해 대체적으로 작은 경향이 있다. 그리고 기본제공 타입에 대해서는 값에 의한 전달을 해도 큰 문제가 없다.
이런 부분에 착안해서 크기가 작은 사용자 정의 타입을 값에 의한 전달로 사용해도 된다고 생각하는 사람이 있을 수 있다. 타입의 크기가 작다고 복사 생성자 호출이 저비용일 것이라는 생각은 위험하다. 타입의 크기가 작더라도, 데이터 멤버가 포인터라면 이러한 객체를 복사할 때에는 포인터와 함께 포인터가 가리키는 대상도 복사해야 한다.
객체의 크기도 작고 복사 생성자의 비용도 크지 않아도 수행 성능 문제가 발목을 잡을 수 있다. 컴파일러 중에는 기본제공 타입과 사용자 정의 타입을 아예 다르게 취급하는 것들이 있다. 이런 경우 기본제공 타입은 레지스터에 넣어 사용할 수 있지만, 사용자 정의 타입은 레지스터에 넣어주지 않는 경우가 있다.
더구나 현재 사용자 정의 타입에 크기가 작더라도, 사용자 정의 타입은 언제나 변화에 노출되어 있기 때문에 미래에 어떻게 될지 장담할 수 없다.
항목 21: 함수에서 객체를 반환해야 할 경우에 참조자(레퍼런스)를 반환하려고 들지 말자
- 레퍼런스는 그냥 이름이다. 이미 존재하고 있는 객체에 붙는 다른 이름이라고 생각해야 한다.
- 따라서 어떤 함수의 반환값이 레퍼런스 형식이라면, 이 함수가 반환하는 레퍼런스는 반드시 이미 존재하는 객체의 레퍼런스여야만 한다.
- C++에서 함수 수준에서 새로운 객체를 만드는 방법은 딱 두 가지가 있다.
- 하나는 스택에 만드는 것
- 다른 하나는 힙에 만드는 것
- 스택에 만든 객체를 레퍼런스로 반환할 때의 문제는 다음과 같다.
- 스택에서 만든 객체는 지역 변수이기 때문에 함수가 종료되면 소멸한다.
- 따라서 함수에서 레퍼런스를 반환하면, 온전한 객체에 대한 레퍼런스가 아니게 된다.
- 힙에 만든 객체를 레퍼런스로 반환할 때의 문제는 다음과 같다.
- 힙에 객체를 만들려면 new 연산자를 사용해야 한다.
- new가 호출되었으면 delete를 호출해주어야 하는데, 어떤 객체가 delete를 호출해주어야 할까?
- 사용자 입장에서는 delete를 호출해줄 수 있는 방법도 없다.
- 왜냐하면 참조자 뒤에 숨겨진 포인터에 대해서 접근할 수 있는 방법이 없기 때문이다.
- 새로운 객체를 반환해야 하는 함수에서는 새로운 객체를 반환하게 만드는 것이 정도이다.
메모
항목 20: ‘값에 의한 전달’보다는 상수객체 참조자(레퍼런스)에 의한 전달’ 방식을 택하는 편이 대개 낫다
- 값에 의한 전달 보다는 상수 객체 레퍼런스에 의한 전달을 선호하자. 대체적으로 효율적일 뿐만 아니라 복사손실 문제까지 막아준다.
- 다만, 기본제공 타입이나 STL, 반복자, 그리고 함수 객체 타입에는 맞지 않는다. 이들은 값에 의한 전달이 더 적절하다.(대신 복사 효율을 높이고, 복사손실 문제가 없도록 만들어야 한다.)
항목 21: 함수에서 객체를 반환해야 할 경우에 참조자(레퍼런스)를 반환하려고 들지 말자
- 지역 스택 객체에 대한 포인터나 참조자를 반환하는 일, 혹은 힙에 할당된 객체에 대한 참조자를 반환하는 일, 또는 지역 정적 객체에 대한 포인터나 참조자를 반환하는 일은 그런 객체가 두 개 이상 필요해질 가능성이 있다면 절대 하면 안된다.
'책 > 이펙티브 C++' 카테고리의 다른 글
이펙티브 C++(169p ~ 182p) (2) | 2024.01.22 |
---|---|
이펙티브 C++(160p ~169p) (1) | 2024.01.21 |
이펙티브 C++(142p ~ 149p) (0) | 2024.01.18 |
이펙티브 C++(132p ~ 142p) (0) | 2024.01.17 |
이펙티브 C++(121p ~ 132p) (0) | 2024.01.16 |