| 일 | 월 | 화 | 수 | 목 | 금 | 토 |
|---|---|---|---|---|---|---|
| 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 |
Tags
- 게임
- 오블완
- Effective c++
- 상속
- implicit conversion
- 영화 리뷰
- 암시적 변환
- virtual function
- Smart Pointer
- 참조자
- 반복자
- resource management class
- 보편 참조
- operator new
- more effective c++
- 예외
- lua
- effective modern c++
- std::async
- 스마트 포인터
- 티스토리챌린지
- effective stl
- exception
- c++
- iterator
- 언리얼
- universal reference
- UE4
- reference
- 영화
Archives
- Today
- Total
스토리텔링 개발자
[Effective Modern C++] 41. 항상 복사되는 매개변수는 값 전달도 고려하기 본문
Effective C++/Effective Modern C++
[Effective Modern C++] 41. 항상 복사되는 매개변수는 값 전달도 고려하기
김디트 2025. 4. 29. 11:15728x90
항목 41. 이동이 저렴하고 항상 복사되는 복사 가능 매개변수에 대해서는 값 전달을 고려하라
복사되도록 만들어진 함수 매개변수
class Widget
{
public:
// 이 버전은 복사되도록 만들어졌다.
void addName(const std::String& newName) // lValue
{
names.push_back(newName); // 복사
}
// 이동 버전
void addName(std::string&& newName) // rValue
{
names.push_back(std::move(newName)); // 이동
}
...
private:
std::vector<std::string> names;
};
- 이 경우 본질적으로 같은 일을 하는 함수를 두 개 작성해야 한다.
- 번거롭고, 유지보수에도 안 좋다.
- 목적 코드(object code)에도 함수가 두 개 존재하게 된다.
- 인라인화가 확실히 되지 않는다면 실행 프로그램의 크기가 커진다.
- 대안 : addName 함수를 보편 참조(항목 24 참조)를 받는 함수 템플릿으로 만든다.
class Widget
{
public:
template<typename T>
void addName(T&& newName)
{
names.push_back(std::forward<T>(newName));
}
...
};
- 소스코드는 줄어들지만, 보편 참조를 사용하므로 그와 관련된 곤란한 문제점들이 발생할 수 있다.
- 또한 템플릿이므로 반드시 헤더 파일에 두어야 한다.
- 목적 코드에는 이 템플릿의 서로 다른 인스턴스가 여러 개 포함될 수 있다.
- lValue, rValue에 대해 다르게 인스턴스화 될 뿐 아니라, std::string이나 이로 변환 가능한 타입들에 대해서도 다르게 인스턴스화 된다.(항목 25 참조)
- 보편 함수로는 전달할 수 없는 인수 타입이 존재한다.(항목 30 참조)
- 사용자 측에서 부적절한 타입의 인수를 전달하면 컴파일러가 난해한 오류 메시지를 출력한다.(항목 27 참조)
둘 모두를 충족시키는 법
- 커스텀 타입 객체는 값으로 전달하지 말라고 배웠을 테지만...
- 이 경우에는 그 규칙을 포기하도록 한다.
class Widget
{
public:
void addName(std::string newName) // lValue나 rValue
{
names.push_back(std::move(NewName)); // 이동
}
...
};
- std::move를 사용한 이유?
- newName은 호출하는 측에서 보낸 것과는 다른, 독립적인 객체이다.
- 해당 위치가 newName을 마지막으로 사용하는 곳이므로 다른 대상으로 이동해도 된다.
- addName 함수가 단 하나이므로 소스 코드와 목적 코드에서 코드 중복이 없다.
- 헌데 값 전달은 비용이 크지 않나...?
C++11에서의 값 전달
- C++98에서는 실제로 비용이 컸다.
- 호출하는 측에서 뭘 넘겨주든, 매개변수 newName은 복사 생성에 의해 생성되었다.
- 하지만 C++11에서는 인수가 lValue일 때만 복사 생성하고, rValue일 때는 이동 생성(move construction)한다.
Widget w;
...
std::string name("Bart");
w.addName(name); // lValue 호출. 복사 생성.
...
w.addName(name + "Jenne"); // rValue 호출. 이동 생성.
위에서 소개한 각 방법에 대한 고찰
- 바로 위의 예제(w.addName())을 통해 확인해보자.
- overload 구현
- 호출측에서 넘겨준 인수가 lValue이든 rValue이든 인수는 newName에 묶인다.(비용이 없다.)
- lValue 오버로드 함수에서는 newName이 Widget::names로 복사된다.
- rVlaue 오버로드 함수에서는 newName이 Widget::names로 이동된다.
- 비용 정리
- lValue : 복사 1회
- rValue : 이동 1회
- 보편 참조 구현
- 호출측에서 넘겨준 인수는 참조 newName에 묶인다.(비용이 없다.)
- std::forward 때문에 lValue std::string 인수는 Widget::names에 복사된다.
- std::forward 때문에 rValue std::string 인수는 Widget::names에 이동된다.
- 비용 정리
- lValue : 복사 1회
- rValue : 이동 1회
- 허나, 넘겨준 인수가 std::string 이외의 타입이면 그 인수는 std::string 생성자로 전달된다.
- 그러면 std::string 복사 혹은 이동 연산이 0회 이상 일어날 수 있다.
- 따라서 보편 참조를 받는 함수는 대단히 효율적일 수 있다.
- 하지만 이번 분석과는 관련 없으므로 논의를 간단하게 하기 위해 항상 std::string을 인수로 받는다고 가정.
- 값 전달 구현
- 호출측에서 넘겨준 인수가 lValue이든 rValue이든 매개변수 newName이 반드시 생성된다.
- 인수가 lValue면 복사 생성 1회
- 인수가 rValue면 이동 생성 1회
- 함수 본문에서는 어떤 경우든 newName이 항상 Widget::names로 이동된다.
- 비용 정리
- lValue : 복사 1회, 이동 1회
- rValue : 이동 2회
- 즉 참조 전달 접근방식들에 비해 이동이 하나 더 많다.
- 호출측에서 넘겨준 인수가 lValue이든 rValue이든 매개변수 newName이 반드시 생성된다.
값 전달을 선택할 때 주의사항
- 값 전달을 '사용하라'가 아니라 '고려하라'.
- 함수를 하나만 작성하면 된다.
- 목적 코드가 함수 하나만 만들어진다.
- 보편 참조와 관련된 문제점이 없다.
- 하지만 다른 대안들보다 비용이 크다.
- 복사 가능 매개변수에 대해서만 고려해야 한다.
- 복사할 수 없는 매개변수는 반드시 이동 전용 타입일 것이다.
- 오버로드 방식에 비해 유리한 이유는 함수를 하나만 작성하면 된다는 부분이다.
- 헌데 이동 전용 타입은 어차피 함수 하나만 작성하면 된다.
- 이동이 저렴한 매개변수에 대해서만 고려해야 한다.
- 이동 1회가 크리티컬하다면 이는 불필요한 복사를 수행하는 것이나 마찬가지이다.
- 항상 복사되는 매개변수에 대해서만 고려해야 한다.
- 만일 조건에 따라 복사를 행한다면 불필요한 생성, 파괴 비용을 유발한다.
-
class Widget { public: void addName(std::string newName) { // 조건에 따라 복사가 발생한다. if((newName.length() >= minLen) && (newName.length() <= maxLen)) { names.push_back(std::move(newName)); } // 복사가 발생하지 않았다면, newName 생성, 파괴 비용만 나갈 뿐이다. } ... private: std::vector<std::string> names; };
- 이동이 저렴한 복사 가능 타입이고 늘 복사를 수행하는 함수라 해도 값 전달이 적합하지 않은 경우가 있다.
- 함수가 매개변수를 복사하는 방식이 두 가지이기 때문이다.
- 생성을 통한 복사(복사 생성이나 이동 생성)
- 할당을 통한 복사(복사 할당이나 이동 할당)
- 앞선 예, addName은 생성을 사용한다.
- 할당을 통해 복사하는 함수에서는 상황이 좀 더 복잡하다.
-
class Password { public: explicit Password(std::string pwd) // 값 전달 : text(std::move(pwd)) {} // text를 생성 void changeTo(std::string newPwd) // 값 전달 { text = std::move(newPwd); // text를 할당 } ... private: std::string text; }; std::string initPwd("Supercalifragilisticexpialidocious"); Password p(initPwd); // 값 전달 -> 생성을 통한 복사 std::string newPassword = "Beware the Jabberwock"; p.changeTo(newPassword); // 값 전달 -> 할당을 통한 복사 - 할당으로 복사를 수행하면 비용이 아주 커질 수 있다.
- 메모리 할당 문제
- changeTo에 전달된 인수는 lValue이다.
- 매개변수 newPwd가 생성될 때 호출되는 것은 std::string의 복사 생성자이다.
- 그 생성자는 새 패스워드를 담을 메모리를 할당한다.
- 이후 newPwd가 text로 이동 할당된다.
- 그 시점에 text가 차지하고 있던 메모리가 해제된다.
- 따라서 changeTo 안에서 동적 메모리 관리 동작이 두 번 발생한다.
- 새 패스워드를 담을 메모리 할당
- 기존 메모리가 차지하던 메모리를 해제
- 헌데 지금 예에서는 기존 패스워드가 새 패스워드보다 길기 때문에 새롭게 메모리를 할당할 필요가 없다.
- 오버로드 방식을 사용하면 메모리 할당 / 해제를 완전히 생략할 수 있다.
-
class Password { public: ... void changeTo(const std::string& newPwd) // lValue를 위한 버전 { // text.capacity() >= newPwd.siz()라면 text 메모리를 재사용할 수 있다. text = newPwd; } ... private: std::string text; }; - 물론 기존 패스워드가 새것보다 짧으면 할당 중 메모리 할당-해제를 피할 수는 없다.
- 그 경우 값 전달과 참조 전달은 거의 비슷한 속도로 실행될 것이다.
- 따라서 할당 기반 매개변수 복사의 비용은 할당에 관여하는 객체의 값에 의존한다.
- 이런 비용 증가는 lValue 인수 전달시에만 적용된다.
- 메모리 할당 해제는 진짜 복사 연산(이동 연산이 아닌!)이 수행될 때만 필요하다.
- 즉, 최대한 빨라야 하는 소프트웨어에서는 값 전달이 그리 바람직하지 않다.
- 더군다나 값 전달 함수에서 이동 연산이 몇 번일어나는지 확실하지 않은 경우도 있다.
- Widget::addName 함수 내에서 또 다른 값 전달 방식 함수를 호출한다면?
- 함수가 매개변수를 복사하는 방식이 두 가지이기 때문이다.
- 값 전달에서는 슬라이스 문제가 발생할 수 있다.
728x90
'Effective C++ > Effective Modern C++' 카테고리의 다른 글
| [Effective Modern C++] 42. 삽입(insert) 대신 생성 삽입(emplace) 고려하기 (0) | 2025.04.30 |
|---|---|
| [Effective Modern C++] 40. std::atomic, volatile (0) | 2025.04.24 |
| [Effective Modern C++] 39. void future 객체 (0) | 2025.04.17 |
| [Effective Modern C++] 38. 스레드 핸들 소멸자의 동작 (0) | 2025.04.15 |
| [Effective Modern C++] 37. std::thread는 unjoinable하게 (0) | 2025.04.11 |
Comments