일 | 월 | 화 | 수 | 목 | 금 | 토 |
---|---|---|---|---|---|---|
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 |
- private상속
- fvf
- 이른진단
- 정점버퍼
- most vexing parse
- RAII
- sharedptr
- 템플릿
- 암시적인터페이스
- rcsp
- 가상기본클래스
- 교차dll문제
- uniqueptr
- EffectiveC++
- 상수객체참조
- operator=
- 부분복사
- Directx9
- c++
- effectivec++.
- 복사함수
- 게임프로그래밍
- 도우미함수
- 제네릭프로그래밍
- 일반화복사생성자
- 스택풀림
- contextswitching
- 해골책
- 멤버함수템플릿
- 암시적변환
- Today
- Total
성공할 게임개발자
[Effective c++] 20. "값에 의한 전달"보다는 "상수 객체 참조자에 의한 전달"방식을 택하는 편이 대게 낫다. 본문
기본적으로 c++은 함수로부터 객체를 전달받거나 함수에 객체를 전달할 때 "값에 의한 전달(pass-by-value)" 방식을 사용한다. 이 것의 근간이 복사 생성자이고 이 점 때문에 "값에 의한 전달(pass-by-value)"이 고비용의 연산이 된다.
문제 상황
class Person {
public:
Person();
virtual ~Person(); // 항목7 가상소멸자(이 객체는 기본클래스)
...
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 객체를 값으로 전달받는 함수
Student plato;
bool platoIsOK = validateStudent(plato);
파생클래스인 Plato로 부터 validateStudent함수의 인자로 받은 s를 초기화 하기 위해 Student의 복사생성자가 호출된다. 하지만 Student객체에는 String 객체가 두 개가 멤버로 들어있고 또 Student객체가 초기화 되기전에 Person의 생성자도 호출되는데 여기에도 string이 있다.
결론은 student객체 하나를 값으로 전달했는데 복사생성자 호출이 너무 많다는 것이다.
해결방법
상수객체에 대한 참조자(reference-to-const)로 전달하게 만들면 된다.
bool validateStudent(const Student& s);
1. 참조자로 받았기 때문에 생성,소멸자 호출하지않음, const붙혀 객체의 불변성 보존.
참조자로 받으면 새로 만들어지는 객체가 없기 때문에 생성자와 소멸자가 호출되지 않는다. 주의 깊게 봐야할 점은 const이다.
const가 붙은 이유는 Student객체의 전달 방식이 참조에 의한 전달이기 때문이다. 이것이 붙어야 함수로 넘어간 객체가 불변하다는 것을 보장할 수 있다.
2. 복사손실 문제(Slicing problem) 방지
파생클래스가 기본클래스 객체로서 전달되는 경우가 많은데(업 캐스팅) 이 객체가 값으로 전달 시 기본클래스의 복사생성자가 호출되어 파생 클래스 객체로 동작하게 해주는 특징이 거세되고 만다. 왜냐면 기본클래스 생성자가 만들었으니까.
class Window {
public:
...
std::string name() const; // 윈도우의 이름 반환
virtual void display() const; // 윈도우를 화면에 그림
};
class WindowWithScrollBars: public Window {
public:
...
virtual void display() const; //파생에서 개선된 display
};
//===========================================================
//값에의한 복사로 매개변수가 복사손실 당함
void printNameAndDisplay(Window w)
{
cout << w.name();
w.display();
}
WindowWithScrollBars wwsb;
printNameAndDisplay(wwsb);
이와 같이 값에 의한 복사로 전달 시에 기본 클래스(Window)의 생성자로 객체가 생성되, 파생클래스 WindowWithScrollBars 객체의 정보가 잘려나가 파생클래스의 display함수의 정보가 유실된다.
해당문제 역시 상수객체에 대한 참조자로 전달하도록 만들면 된다.
//const 레퍼런스 참조로 이제 파생 부분이 유실되지 않는다
void printNameAndDisplay(const Window& w)
{
cout << w.name();
w.display();
}
추가 내용
참조자는 포인터를 써서 구현된다. 참조자를 전달한다는 것은 포인터를 전달하는 것과 똑같다. 하지만 기본제공타입(int..등)이나 STL 반복자(iterator)와 함수객체는 상수객체의 참조보다 값으로 전달하는 경우가 낫다. 왜냐면 이렇게 설계되었기 때문이다.
반복자와 함수 객체를 구현할 때는 반드시 복사 효율을 높일 것과 복사손실 문제에 신경쓰는 것이 필수다.
크기자 작다고 해서 작은 사용자 정의 타입을 무조건 값으로 전달하지 말자
사용자 정의 타입의 크기가 작다고해서 값으로 넘기는 것은 좋지않다.
1.크기가 작다고 그것의 복사생성자 호출이 저비용이 아니다.
객체를 복사하는데는 그 포인터멤버가 가리키는 대상까지 복사하는 경우가 있기 때문이다. 즉, 크기가 작아도 비쌀 수 있다.
2.수행성능 문제
예를들어 기본제공타입인 double은 컴퓨터가 이것에 대한 메모리를 레지스터에 넣어주는데, double을 가지고 있는 사용자 정의타입 객체는 메모리가 레지스터에 들어가지 않는다. 하지만 포인터(참조자가 포인터로 구현 됨)는 레지스터에 확실하게 들어간다.
3.사용자 정의타입이 지금 작다고 미래에 안 커질거란 보장이 없다.
지금은 크기가 작아도 기능을 추가하고 추가하고 하다보면 크기가 커질 수 있다. 예를 들어 c++초기의 string과 지금 string은 크기가 7배나는 경우도 있다고 한다.
기본 제공 타입, STL 반복자, 함수 객체 타입 이렇게 3가지를 제외하고는 "상수객체 참조자에 의한 전달"을 사용하자. 그냥 하라면 해.
추가) 함수객체 타입
#include <iostream>
struct Adder {
int operator()(int a, int b) const {
return a + b;
}
};
int main() {
Adder add;
std::cout << add(3, 5) << std::endl; // 출력: 8
}
중요포인트!
"값에 의한 전달" 보다는 "상수 객체 참조자에 의한 전달"을 선호하자. 대체적으로 효율적일 뿐만 아니라 복사손실 문제까지 막아준다.
이번항목에서 다룬 "상수 객체 참조자에 의한 전달"은 기본 제공 타입, STL 반복자, 함수 객체 타입에는 맞지 않는다. 이들에 대해서는 "값에 의한 전달"이 더 낫다.
'C++' 카테고리의 다른 글
[Effective c++] 22. 데이터 멤버가 선언될 곳은 private 영역임을 명심하자 (0) | 2025.06.05 |
---|---|
[Effective c++] 21. 함수에서 객체를 반환해야 할 경우에 참조자를 반환하려고 들지 말자 (1) | 2025.06.04 |
[Effective c++] 19. 클래스 설계는 타입 설계와 똑같이 취급하자 (0) | 2025.06.02 |
[Effective c++] 18. 인터페이스 설계는 제대로 쓰기엔 쉽게, 엉터리로 쓰기엔 어렵게 하자 (0) | 2025.06.01 |
[Effective c++] 17. new로 생성한 객체를 스마트 포인터에 저장하는 코드는 별도의 한 문장으로 만들자 (0) | 2025.05.30 |