주간 팁 #176: 출력 매개변수 대신 반환 값을 선호하세요

원래 TotW #176으로 2020년 3월 12일 게시됨
작성자: Etienne Dechamps
2020-04-06 업데이트됨

빠른 링크: abseil.io/tips/176


문제

다음과 같은 함수를 고려해봅시다:

CPP
// 주어진 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 객체를 미리 할당해야만 함수를 호출할 수 있습니다.

이 경우, 보일러플레이트를 제거하고 컴파일러가 계약을 강제할 수 있도록 간단히 해결할 수 있습니다.


해결책

다음과 같이 모든 문제를 해결할 수 있습니다:

CPP
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)라면, 출력 매개변수 접근법에서는 호출자가 특정 아레나에서 해당 프로토를 할당할 수 있습니다. 반면 반환값 접근법에서는 아레나를 추가 매개변수로 지정하거나 호출자가 이를 미리 알고 있어야 합니다.
  • 성능은 선택한 접근법과 상황에 따라 달라질 수 있습니다.
    • 반환값 사용이 비효율적일 수 있는 경우도 있습니다. 예를 들어, 반복문 내에서 반복적으로 할당이 발생할 경우입니다.
    • 그러나 반환값 사용이 예상보다 효율적인 경우도 있습니다. (N)RVO(팁 #11, 팁 #166) 덕분입니다. 심지어 출력 매개변수보다 더 효율적일 수 있습니다. 왜냐하면 옵티마이저가 별칭(aliasing)에 대해 걱정할 필요가 없기 때문입니다.
    • 항상 조기 최적화를 피하세요. 가장 적합한 API를 선택하고, 성능이 실제로 영향을 미친다는 증거가 있을 때만 최적화에 신경 쓰세요.

권장 사항

  1. 출력 매개변수 대신 반환값을 선호하세요. 이는 스타일 가이드 와 일치합니다.
  2. 반환값이 없음을 표현하려면 std::optional과 같은 일반 래퍼를 사용하세요. 더 유연한 표현이 필요하다면 std::variant를 고려하세요.
  3. 구조체를 사용하여 함수에서 여러 값을 반환하세요.
    • 필요하다면 해당 함수의 반환값을 표현하기 위해 새로운 구조체를 만들어도 좋습니다.
    • std::pair나 std::tuple 사용의 유혹에 저항하세요 .

라이선스

작성자: Jaehun Ryu

링크: https://jaehun.me/posts/abseil-tip-176-%EC%B6%9C%EB%A0%A5-%EB%A7%A4%EA%B0%9C%EB%B3%80%EC%88%98-%EB%8C%80%EC%8B%A0-%EB%B0%98%ED%99%98-%EA%B0%92%EC%9D%84-%EC%84%A0%ED%98%B8%ED%95%98%EC%84%B8%EC%9A%94/

라이선스: CC BY 4.0

이 저작물은 크리에이티브 커먼즈 저작자표시 4.0 국제 라이선스에 따라 이용할 수 있습니다. 출처를 밝히면 상업적 목적을 포함해 자유롭게 이용 가능합니다.

댓글