anothel의 지식 창고

Google C++ Style Guide(2024) - 5장 함수 (Functions) 본문

기술 노트/Google Code Style Guide

Google C++ Style Guide(2024) - 5장 함수 (Functions)

anothel 2024. 11. 8. 19:00

5장 함수 (Functions)

5.1 입력과 출력 (Inputs and Outputs)

C++ 함수에서 출력을 제공하는 방식으로 반환값을 선호하는 것이 좋다. 반환값은 가독성을 높이며 성능도 유리한 경우가 많다. 다음은 함수의 입력과 출력에 관한 권장 사항이다.

반환값과 출력 매개변수의 사용

  • 반환값
    • 출력을 제공할 때는 반환값을 우선으로 사용한다. 성능과 가독성이 좋다.
  • 출력 매개변수
    • 반환값 대신 출력 매개변수를 사용할 경우, 가능하면 참조를 사용하고 널이 될 수 있는 경우에만 포인터를 사용한다.

함수 매개변수의 종류와 권장 사용 방식

  • 입력 매개변수
    • 보통 값 타입이나 const 참조로 전달한다.
  • 출력 및 입출력 매개변수
    • 비널(non-null)인 경우 참조로 전달하는 것이 좋다.
  • 선택적 매개변수:
    • 선택적 입력 매개변수는 std::optional을 사용하는 것이 권장된다.
    • 선택적 출력이나 입출력 매개변수는 널이 될 수 있는 포인터로 표현할 수 있다.
    • 널이 될 수 없는 선택적 매개변수는 const 포인터로 사용할 수 있다.

매개변수의 수명 문제

참조 매개변수를 사용해 함수 호출 이후에도 객체가 살아 있어야 하는 상황은 피해야 한다. 이러한 경우 수명을 없애는 방식(예: 복사)을 고려하거나 포인터로 전달하며, 수명 요구 사항과 비널(non-null) 조건을 문서화하는 것이 좋다.

매개변수 순서

함수 매개변수의 순서는 입력 매개변수부터 시작하고 출력 매개변수를 뒤에 배치하는 것이 좋다. 특히, 새로운 매개변수를 추가할 때 단순히 마지막에 추가하기보다는, 입력 매개변수는 출력 매개변수 앞에 배치하는 것이 권장된다. 다만, 입출력 매개변수나 가변 인수를 사용할 때는 일관성과 함수의 의미에 따라 규칙을 조정할 수 있다.

이러한 원칙은 함수의 가독성과 안전성을 높이는 데 기여하며, 매개변수와 반환값의 용도를 명확하게 구분해 준다.

5.2 짧은 함수 작성하기 (Write Short Functions)

함수는 작고 집중된 작업만 수행하도록 작성하는 것이 좋다. 긴 함수가 필요할 때도 있지만, 함수가 약 40줄을 넘으면 코드 구조를 해치지 않으면서 분리할 수 있는지 고려해야 한다. 짧고 단순한 함수는 코드의 가독성과 유지보수성을 높이고, 테스트도 용이하게 만든다.

긴 함수의 문제점과 해결 방법

긴 함수는 현재 완벽하게 동작하더라도, 나중에 수정하거나 새로운 기능을 추가할 때 오류가 발생할 위험이 있다. 긴 함수는 디버깅이 어렵고, 재사용하고 싶을 때도 불편하다. 따라서 짧고 단순한 함수로 유지하는 것이 바람직하다.

기존 코드에서 긴 함수 다루기

다른 사람이 작성한 긴 함수나 복잡한 코드를 다루어야 할 때는, 주저하지 말고 필요에 따라 함수 분할을 고려하자. 특히, 긴 함수가 다루기 어렵거나 디버깅이 힘들고, 코드의 일부를 여러 문맥에서 사용해야 할 때 함수를 작고 관리하기 쉬운 단위로 나누는 것이 좋다.

결론적으로, 작은 함수는 코드의 가독성과 유지보수성을 높이며, 더 간편하게 테스트할 수 있도록 해준다.

5.3 함수 오버로딩 (Function Overloading)

오버로딩을 사용할 때는 호출된 오버로드가 어떤 역할을 수행하는지 명확히 파악할 수 있도록 작성하는 것이 중요하다. 호출 지점에서 어떤 오버로드가 호출될지 쉽게 예상할 수 있어야 한다.

오버로딩의 유용성

  • 함수 오버로딩은 같은 이름의 함수가 다른 인수를 받을 수 있도록 해주므로, 코드가 직관적이고 가독성이 높아진다.
  • 템플릿 코드나 방문자(Visitor) 패턴을 구현할 때 유용하다.
  • const 또는 참조에 따른 오버로딩은 유틸리티 코드의 사용성과 효율성을 높일 수 있다.

예시)

class MyClass {
 public:
  void Analyze(const std::string &text);
  void Analyze(const char *text, size_t textlen);
};

위의 예에서 Analyze 함수는 std::string과 const char* 타입을 각각 받아들이는 두 가지 버전으로 오버로딩되어 있다. 하지만 이 경우 std::string_view를 사용하는 것이 더 유용할 수 있다.

오버로딩 시 주의사항

  • 매개변수 타입으로만 오버로딩하는 경우, 호출 지점에서 어떤 함수가 호출될지 파악하기 어렵다. 특히 C++의 복잡한 매칭 규칙을 이해해야 하는 상황이 발생할 수 있다.
  • 상속 시 혼동
    • 파생 클래스가 일부 오버로드만 오버라이딩할 경우, 해당 함수의 다른 버전과의 상속 관계가 혼란을 줄 수 있다.

오버로딩 사용 권장 조건

  • 의미적 차이가 없는 경우에만 오버로딩을 사용해야 한다. 즉, 오버로드된 모든 함수가 동일한 의미를 공유해야 한다.
  • 오버로드된 함수들 간에 타입, 한정자(const 등), 매개변수 수의 차이만 있을 때 적절하다.
  • 단일 주석으로 모든 오버로드 함수를 설명할 수 있는 경우 좋은 설계의 오버로드 집합이라고 볼 수 있다. 호출 지점에서 어느 오버로드가 호출되는지 구체적으로 알 필요 없이, 오버로드 집합 중 하나가 호출됨을 이해할 수 있어야 한다.

함수 오버로딩을 사용할 때는 명확하고 일관된 의미를 유지하여 코드의 가독성을 높이고, 코드 사용자(개발자)가 혼란을 겪지 않도록 하는 것이 핵심이다.

5.4 기본 인수 (Default Arguments)

기본 인수는 가상 함수가 아닌 함수에서만 사용하며, 기본값이 항상 일정하게 유지되는 경우에만 사용하는 것이 좋다. 기본 인수는 코드를 간결하게 만들어주지만, 함수 오버로딩보다 가독성이 높지 않다면 오버로딩을 선호하는 것이 좋다.

기본 인수의 장점과 제한

기본 인수는 필요할 때만 기본값을 변경하고, 나머지 경우에는 기본값을 사용하도록 해주므로 여러 함수를 정의하지 않고도 쉽게 사용할 수 있다. 기본 인수를 사용하면 오버로딩에 비해 더 간결한 문법을 제공하고, 필수 인수와 선택적 인수를 명확히 구분할 수 있다.

하지만 기본 인수는 함수 오버로딩과 유사한 의미를 가지므로, 오버로딩을 피해야 하는 이유가 기본 인수에도 적용된다.

가상 함수에서 기본 인수 사용 금지

기본 인수를 가상 함수에서 사용하는 것은 금지되어 있다. 가상 함수에서 기본 인수를 사용할 경우, 기본값이 객체의 정적 타입에 따라 결정되며, 모든 파생 클래스가 동일한 기본값을 선언할 보장이 없다. 이는 코드의 혼란을 초래할 수 있다.

기본 인수의 재평가 문제

기본 인수는 각 호출 지점에서 다시 평가되므로, 생성된 코드가 비대해질 수 있다. 사용자는 기본값이 선언된 위치에서 고정되어 있을 것으로 예상할 수 있지만, 실제로 호출할 때마다 변동될 가능성이 있다.

예를 들어, 다음과 같은 기본값 설정은 피해야 한다.

void f(int n = counter++);  // 호출 시마다 counter++가 평가되어 예상치 못한 값이 될 수 있음

함수 포인터와 기본 인수

기본 인수는 함수 포인터와 함께 사용될 때 혼란을 초래할 수 있다. 함수 포인터의 서명이 호출 서명과 일치하지 않을 수 있기 때문에, 이러한 경우에는 오버로딩을 사용하는 것이 좋다.

기본 인수 사용을 고려할 때

기본 인수를 사용해 함수 선언이 더 간결해지고 가독성이 향상된다면, 앞서 언급한 단점들을 감수하고 사용할 수 있다. 의심이 들 경우, 오버로딩을 우선 고려하는 것이 좋다.

5.5 후행 반환 타입 구문 (Trailing Return Type Syntax)

C++에서는 함수 선언 시 전통적인 반환 타입 위치와 후행 반환 타입 구문을 모두 사용할 수 있다. 대부분의 경우, 전통적인 방식을 사용하는 것이 좋다. 후행 반환 타입은 복잡한 타입이나 람다의 반환 타입을 명시할 때, 혹은 일반적인 구문이 읽기 어려운 경우에만 사용한다.

전통적인 반환 타입 방식과 후행 반환 타입 방식의 차이

  • 전통적인 방식: 반환 타입을 함수 이름 앞에 명시한다.
int foo(int x);
  • 후행 반환 타입 방식: auto 키워드와 함께 반환 타입을 함수 인자 목록 뒤에 명시한다.
auto foo(int x) -> int;

후행 반환 타입은 함수의 스코프 안에 속하므로, 클래스 내부 타입이나 함수 매개변수에 의존하는 복잡한 타입을 정의할 때 유리하다.

후행 반환 타입이 필요한 경우

  • 람다 표현식: 람다에서 반환 타입을 명시하려면 후행 반환 타입이 필요하다. C++ 컴파일러가 대부분 자동으로 추론하지만, 가독성을 위해 명시할 수 있다.
auto lambda = [](int x) -> int { return x * 2; };
  • 템플릿 및 복잡한 타입: 템플릿의 반환 타입이 함수 매개변수에 의존하는 경우, 후행 반환 타입이 가독성에 유리하다.
template <typename T, typename U>
auto add(T t, U u) -> decltype(t + u);

전통적인 방식으로 작성할 경우 훨씬 복잡해질 수 있다.

template <typename T, typename U>
decltype(declval<T&>() + declval<U&>()) add(T t, U u);

권장 사항

  • 일관성 유지
    • 대부분의 코드에서 전통적인 반환 타입 방식을 사용하므로, 코드의 일관성을 위해 이 방식을 선호한다. 후행 반환 타입은 특별한 경우에만 사용하고, 가급적이면 일관된 스타일을 유지하는 것이 좋다.
  • 복잡한 템플릿 코드에서만 사용
    • 후행 반환 타입은 주로 복잡한 템플릿 코드에서 필요할 수 있지만, 지나치게 복잡한 템플릿 코드 작성을 지양하는 것이 일반적인 권장사항이다.

결론

후행 반환 타입 구문은 필요할 때만 사용하는 것이 좋다. 일반적인 함수 선언에서는 전통적인 반환 타입 방식이 일관성과 가독성 면에서 유리하다.


참조URL

https://google.github.io/styleguide/cppguide.html

728x90