주간 팁 #176: 출력 매개변수 대신 반환 값을 선호하세요
원래 TotW #176으로 2020년 3월 12일 게시됨
작성자: Etienne Dechamps
2020-04-06 업데이트됨
빠른 링크: abseil.io/tips/176
문제
다음과 같은 함수를 고려해봅시다:
// 주어진 doodad에서 foo 사양과 bar 사양을 추출합니다.
// 입력이 유효하지 않으면 false를 반환합니다.
bool ExtractSpecs(Doodad doodad, FooSpec* foo_spec, BarSpec* bar_spec);
이 함수를 올바르게 사용하거나 구현하려면 개발자는 다음과 같은 질문을 해야 합니다:
foo_spec
과bar_spec
이 출력 매개변수인지, 아니면 입출력 매개변수인지?foo_spec
과bar_spec
의 기존 데이터는 어떻게 처리되나요? 덮어쓰나요? 추가되나요? 함수가CHECK-fail
을 일으키나요? 아니면false
를 반환하나요? 정의되지 않은 동작인가요?foo_spec
과bar_spec
이 null일 수 있나요? null일 수 없다면, null 포인터가 전달되었을 때CHECK-fail
을 발생시키나요? 아니면false
를 반환하나요? 정의되지 않은 동작인가요?foo_spec
과bar_spec
의 수명 요구사항은 무엇인가요? 즉, 함수 호출보다 오래 살아남아야 하나요?false
가 반환되었을 때,foo_spec
과bar_spec
은 어떻게 되나요? 변경되지 않도록 보장되나요? 특정 방식으로 “초기화”되나요? 아니면 명시되지 않나요?
이 질문들에 함수 시그니처만으로는 답할 수 없습니다. 또한 C++ 컴파일러가 이러한 계약을 강제하지 않습니다. 함수 주석은 도움이 될 수 있지만, 종종 충분하지 않습니다. 위 함수의 문서화는 대부분의 문제를 다루지 않고 있으며, “입력”이라는 의미조차도 모호합니다. 이는 doodad
만을 의미하는지, 아니면 다른 매개변수까지 포함하는지를 명확히 하지 않습니다.
게다가, 이 접근법은 모든 호출 위치에 보일러플레이트 코드를 요구합니다. 호출자는 FooSpec
과 BarSpec
객체를 미리 할당해야만 함수를 호출할 수 있습니다.
이 경우, 보일러플레이트를 제거하고 컴파일러가 계약을 강제할 수 있도록 간단히 해결할 수 있습니다.
해결책
다음과 같이 모든 문제를 해결할 수 있습니다:
struct ExtractSpecsResult {
FooSpec foo_spec;
BarSpec bar_spec;
};
// 주어진 doodad에서 foo 사양과 bar 사양을 추출합니다.
// 입력이 유효하지 않으면 nullopt를 반환합니다.
std::optional<ExtractSpecsResult> ExtractSpecs(Doodad doodad);
이 새로운 API는 의미적으로 동일하지만, 오용하기 훨씬 어려워졌습니다:
- 입력과 출력이 무엇인지 더 명확합니다.
foo_spec
과bar_spec
의 기존 데이터에 대한 의문이 사라집니다. 함수에서 새로 생성하기 때문입니다.- 포인터가 없기 때문에 null 포인터에 대한 의문도 사라집니다.
- 모든 것이 값으로 전달되고 반환되므로 수명에 대한 의문도 없습니다.
- 실패 시
foo_spec
과bar_spec
이 어떻게 되는지에 대한 의문도 없습니다.nullopt
이 반환되면 접근조차 할 수 없기 때문입니다.
이로 인해 버그 가능성이 줄어들고 개발자의 인지 부담이 줄어듭니다.
또 다른 장점은 함수가 더 쉽게 조합 가능하다는 것입니다. 예를 들어, SomeFunction(ExtractSpecs(...))
와 같은 방식으로 간단히 사용할 수 있습니다.
주의사항
- 이 접근법은 입출력 매개변수에는 적합하지 않습니다.
- 경우에 따라 매개변수를 값으로 받고, 수정한 뒤 값을 반환하는 방식으로 변형할 수 있습니다. 이 방식이 적합한지는 함수 사용 방식과 값이 효율적으로 이동 가능한지 여부에 따라 달라집니다. (팁 #117 참고)
- 이 접근법은 호출자가 반환된 객체의 생성을 쉽게 사용자 정의할 수 없게 만듭니다.
- 예를 들어,
FooSpec
과BarSpec
이 프로토(protos)라면, 출력 매개변수 접근법에서는 호출자가 특정 아레나에서 해당 프로토를 할당할 수 있습니다. 반면 반환값 접근법에서는 아레나를 추가 매개변수로 지정하거나 호출자가 이를 미리 알고 있어야 합니다.
- 예를 들어,
- 성능은 선택한 접근법과 상황에 따라 달라질 수 있습니다.