KoreanFoodie's Study

Effective Modern C++ | 항목 41 : 이동이 저렴하고 항상 복사되는 복사 가능 매개변수에 대해서는 값 전달을 고려하라 본문

Tutorials/C++ : Advanced

Effective Modern C++ | 항목 41 : 이동이 저렴하고 항상 복사되는 복사 가능 매개변수에 대해서는 값 전달을 고려하라

GoldGiver 2022. 10. 26. 10:09

C++ 프로그래머의 필독서이자 바이블인, 스콧 마이어스의 Modern Effective C++ 를 읽고 기억할 내용을 요약하고 있습니다. 꼭 읽어보시길 추천드립니다!

항목 41 : 이동이 저렴하고 항상 복사되는 복사 가능 매개변수에 대해서는 값 전달을 고려하라

핵심 :

1. 이동이 저렴하고 항상 복사되는 복사 가능 매개변수에 대해서는 값 전달이 참조 전달만큼이나 효율적이고, 구현하기가 더 쉽고, 산출되는 목적 코드의 크기도 더 작다.
2. 왼값 인수의 경우 값 전달(즉, 복사 생성) 다음의 이동 배정은 참조 전달 다음의 복사 배정보다 훨씬 비쌀 가능성이 있다.
3. 값 전달에서는 잘림 문제가 발생할 수 있으므로, 일반적으로 기반 클래스 매개변수 형식에 대해서는 값 전달이 적합하지 않다.

 

Widget 클래스에 대해, 목록에 이름을 추가하는 함수를 구현한다고 해 보자. 왼값과 오른값 버전을 따로 만들어(std::string &, std::string&& 버전)도 되지만, 보편 참조를 받는 함수 템플릿을 만들어도 된다. 하지만 이는 std::string 으로 변환 불가능한 형식도 인스턴스화하며, 보편 참조로 전달할 수 있는 인수 형식이 존재할 수 있다(항목 30 참고). 따라서, 다음과 같이 코드를 작성하는 것이 나름 깔끔하다.

class Widget {
public:
  void addName(std::string newName)
  { names.push_back(std::move(newName)); }
private:
  std::vector<std::string> names;
};

...

Widget w;

std::string name("Bart");
w.addName(name); // 왼값 호출
w.addName(name + "Jenne"); // 오른값 호출

C++98 에서는 위의 addName 이 언제나 복사 생성을 호출했다면, C++11 에서는 newName 이 왼값이면 복사 생성을, 오른값이면 이동 생성을 수행한다. 

 

사실 값 전달 방식 함수를 작성하는 이유는, 함수의 갯수를 하나로 줄일 수 있다는 장점과 목적 코드에 함수 하나만 만들어진다는 장점, 그리고 보편 참조와 관련된 문제점이 없다는 장점 때문이다. 하지만 값 전달 함수를 우선시 할 경우, 다음 네 가지 사항을 고려해야 한다.

1. 사실 비용을 생각해 보면, 값으로 전달하는 경우, newName 이 왼값이면 복사 생성자 1회와 이동 생성자 1회의 비용이 들며, 오른값이면 이동 생성자 2회의 비용이 든다. 오른값/왼값 중복적재 버전의 경우 각각 이동/복사 생성자 1회의 호출의 비용이 드는 것을 생각하면, 이는 쓸데없는 비용이 든다고 볼 수도 있다(보편참조도 각각 1회).

2. 또한 값 전달 함수는 복사 가능 매개변수에 대해서만 값 전달을 고려해야 한다. 만약 이동 연산만 지원하는 경우, "&&" 를 사용해 오른값 참조를 받아야 한다. 값으로 전달하면 이동 생성이 2회로, '중복적재' 접근 방식의 두배 비용이 든다.3. 값 전달은 이동이 저렴한 매개변수에 대해서만 고려해야 한다.4. 값 전달은 항상 복사되는 매개변수에 대해서만 고려해야 한다. 만약 위 addName 코드에서, 일정 길이 조건을 만족하는 경우에만 이름을 목록에 추가한다고 하자. 값 전달의 경우 조건 만족 여부와 상관없이 값을 복사해야 하지만, 참조는 그런 비용을 지불할 필요가 없다.

또한, 매개변수를 생성이 아닌 배정을 통해 복사하는 함수의 경우, 비효율성이 더 증가할 수 있다. 예를 들어, 패스워드를 변경하는 경우, 새 패스워드를 위한 메모리를 복사 생성하면서 새로 할당하고, 이동 생성자를 이용해 기존 변수의 메모리를 해제하고 재할당을 해야 하므로, 2 번의 할당과 해제를 시행해야 한다. 하지만 중복적재 버전의 경우 메모리를 깔끔하게 재활용할 수도 있을 것이다(기존 패스워드의 capacity 가 새 패스워드의 size() 보다 길다면).

결론적으로, 최대한 빨라야 하는 소프트웨어에서는 값 전달이 그리 바람직하지 않다. 항목의 마지막이 왜 '사용하라' 가 아닌 '고려하라' 인지 생각해 보면, 언뜻 모순적인 이 결론이 와 닿을 것이다.

 

마지막으로, 참조 전달과는 달리 값 전달에서는 잘림 문제(slicing problem) 이 발생할 여지가 있다. 예시를 보고 해당 항목을 마무리하도록 하자.

class Widget { ... }; 
class SpecialWidget: public Widget { ... };

// Widget 과 그로부터 파생된 임의의 형식을 받음
// 잘림 문제가 있다
// 파생 클래스 부분이 "잘려 나감(slice off)"
void processWidget(Widget w);

SpecialWidget sw;

// SpecialWidget 이 아니라 Widget 으로 인식!
processWidget(sw);
Comments