Abseil Tip 148 Overload Sets

원래 TotW #148로 2018년 5월 3일 게시됨

작성: Titus Winters

업데이트: 2020-04-06


“전기 정보를 가지고 사는 것의 한 가지 효과는 정보 과부하 상태에서 항상 살아가는 것입니다. 항상 감당할 수 있는 것보다 많은 정보가 존재합니다.” — 마셜 맥루언

C++ 스타일 가이드에서 가장 강력하고 통찰력 있는 문장 중 하나는 다음과 같습니다:
“오버로드된 함수(생성자 포함)는 호출부를 보는 독자가 어떤 오버로드가 호출되는지 정확히 알아내지 않아도 어떤 일이 벌어지는지 잘 이해할 수 있는 경우에만 사용하세요.”

이 규칙은 표면적으로는 간단해 보입니다: 독자를 혼란스럽게 하지 않는 경우에만 오버로드를 사용하라는 것입니다. 그러나 이 규칙은 현대 API 설계에서 중요한 문제와 연결되며 상당히 의미 있는 영향을 미칩니다. 우선 “오버로드 집합(overload set)”이라는 용어를 정의하고 몇 가지 예제를 살펴보겠습니다.


오버로드 집합이란?

비공식적으로, 오버로드 집합은 이름은 동일하지만 매개변수의 개수, 타입, 혹은 수식어가 다른 함수들의 집합입니다.
(자세한 내용은 오버로드 해석을 참고하세요.)
반환 타입으로는 오버로드할 수 없습니다. 컴파일러는 반환 타입과 상관없이 함수 호출만으로 오버로드 집합에서 호출할 함수를 결정할 수 있어야 합니다.

int Add(int a, int b);
int Add(int a, int b, int c);  // 매개변수 개수가 다를 수 있음

// 반환 타입이 달라도, 매개변수만으로 오버로드를 고유하게 식별할 수 있다면 가능
float Add(float a, float b);

// 하지만 동일한 매개변수 시그니처에 서로 다른 반환 타입이 있으면 컴파일되지 않음
int Add(float a, float b);    // 잘못된 코드 - 반환 타입으로 오버로드 불가

문자열 관련 매개변수

C++를 처음 접할 때 가장 먼저 본 오버로드 중 하나는 다음과 같은 형태일 가능성이 높습니다:

void Process(const std::string& s) { Process(s.c_str()); }
void Process(const char*);

이런 형태의 오버로드가 좋은 이유는 규칙의 문자적 의미와 정신적 의미 모두를 충족하기 때문입니다.
두 경우 모두 문자열 데이터를 처리한다는 점에서 행동의 차이가 없으며, 인라인 전달 함수는 오버로드 집합의 모든 멤버가 동일한 동작을 한다는 것을 명확히 보여줍니다.

이는 중요하면서도 쉽게 간과할 수 있는 부분입니다. Google C++ 스타일 가이드는 이를 명시적으로 서술하지는 않지만, 오버로드 집합의 멤버가 서로 다른 동작을 문서화하면 사용자가 호출된 함수가 무엇인지 알지 못하면 예상할 수 없게 됩니다.
따라서 위와 같은 문자열 예제는 동일한 의미를 가지므로 잘 동작합니다.

반면, 다음과 같은 예제는 적절하지 않습니다:

// 차고 출입구의 장애물을 제거
void open(Gate& g);

// 파일 열기
void open(const char* name, const char* mode ="r");

다행히 네임스페이스 차이로 인해 이러한 함수가 실제로 오버로드 집합을 형성하는 것은 방지됩니다. 그러나 이러한 설계는 좋지 않은 설계입니다. API는 오버로드 집합 수준에서 이해되고 문서화되어야 하며, 개별 함수 수준에서 다루어져서는 안 됩니다.


StrCat

StrCat()은 API 설계에서 오버로드 집합이 유용함을 보여주는 Abseil의 대표적인 예제입니다.
StrCat()은 시간이 지나면서 매개변수 개수와 표현 방식이 변해왔습니다. 과거에는 매개변수 개수가 다양한 함수 집합으로 존재했으나, 현재는 개념적으로 가변 템플릿 함수입니다. 최적화를 위해 작은 개수의 매개변수를 가진 오버로드는 여전히 제공됩니다.

이것은 오버로드 집합의 좋은 예입니다. 매개변수 개수를 함수 이름에 인코딩하는 것은 불편하고 중복적일 수 있으며, StrCat()의 역할은 매개변수 개수와 관계없이 문자열을 변환하고 연결하는 것이므로 이를 분리하지 않아도 됩니다.


매개변수 Sink

표준 라이브러리 및 제네릭 코드에서는 값을 저장하기 위해 const T&T&&를 오버로드하는 경우가 많습니다. 예를 들어, std::vector::push_back()은 다음과 같습니다:

void push_back(const T&);
void push_back(T&&);

결론

오버로드 집합은 개념적으로 간단하지만, 잘못 이해하면 남용될 수 있습니다.
어떤 함수가 선택되었는지 알아야 하는 경우 오버로드를 만들지 마세요.
하지만 올바르게 사용하면 오버로드 집합은 API 설계를 위한 강력한 개념적 도구가 됩니다.
API 설계를 고민할 때 스타일 가이드의 오버로드 집합에 대한 미묘한 차이를 이해하는 것은 매우 유익합니다.