하나의 이름, 여러 버전
서로 다른 종류의 데이터에 대해 같은 연산이 필요한 경우가 자주 있습니다. int를 출력하고, string을 출력하고, double을 출력하는 식이죠. 어떤 언어에서는 printInt, printString, printDouble을 만들어야 할 것입니다. C++에서는 이들 모두에 같은 이름을 줄 수 있고, 매개변수로 구분합니다. 이것이 함수 오버로딩입니다.
규칙은 간단합니다. 매개변수 목록이 다른 한, 여러 함수가 이름을 공유할 수 있습니다. 매개변수의 개수, 타입, 또는 순서가 다르면 됩니다. 컴파일러는 각 호출 지점의 인수를 살펴보고 일치하는 버전을 대신 골라 줍니다.
세 개의 함수, 하나의 이름. 각 호출은 인수에 맞는 매개변수 타입을 가진 버전에 도달합니다. 이것이 바로 std::cout << x가 int, double, string 모두에 대해 똑같이 동작하게 만드는 것입니다. operator<<는 수없이 여러 번 오버로드되어 있습니다.
무엇이 서로 다른 오버로드로 간주되는가
오버로드는 매개변수 목록만으로 구분됩니다. 다음을 바꿀 수 있습니다.
int area(int side); // 매개변수 1개
int area(int width, int height); // 매개변수 2개 -> 다름
double area(double r); // 타입이 다름 -> 다름
void log(string msg, int level); // 순서가 중요하다…
void log(int level, string msg); // …따라서 이것도 다름
이들 각각은 적법하고 별개인 오버로드입니다. 컴파일러는 area라는 이름의 모든 함수로부터 후보 집합을 만든 다음, 인수의 개수와 타입으로 일치 여부를 따집니다.
반환 타입만으로는 충분하지 않다
거의 모든 사람을 걸려 넘어지게 하는 함정이 여기 있습니다. 반환 타입으로는 오버로드할 수 없습니다. 반환값은 오버로드 선택에 아무런 역할도 하지 않습니다. 컴파일러는 무엇이 돌아오는지 보기 훨씬 전에 인수로부터 어느 함수를 호출할지 결정하기 때문입니다.
int convert(double x); // OK
double convert(double x); // 오류: 재정의 - 반환 타입만 다름
이것은 컴파일되지 않습니다. 매개변수 목록이 동일하면 오버로딩 관점에서 두 선언은 같은 함수이며, 재정의 오류가 발생합니다. 결과 타입에 따라 분기하려면 매개변수를 바꾸세요(또는 호출 지점에서 템플릿이나 static_cast를 사용하세요).
오버로딩 해석은 어떻게 승자를 고르는가
호출을 하면 컴파일러는 실행 가능한 모든 오버로드의 순위를 매기고 가장 잘 맞는 것을 고릅니다. 대략 다음 순서로 선호합니다.
- 정확히 일치(변환 불필요).
- 승격(예:
char또는short->int,float->double). - 표준 변환(예:
int->double,double->int, 기반 클래스로의 포인터).
정확히 하나의 오버로드가 다른 모든 것보다 엄격하게 더 나으면 그것이 이깁니다. 정확히 일치가 변환을 이기는 모습을 보세요.
'A'는 char이지만, int로의 승격이 double로의 변환보다 높은 순위이므로 int 오버로드가 호출됩니다. 이러한 순위 규칙이 바로 오버로딩 해석이 보통 "딱 맞는 일을 하는" 이유이며, 가끔 당신을 놀라게 하는 이유이기도 합니다.
모호성의 함정
두 오버로드가 똑같이 좋고 어느 것도 엄격하게 더 낫지 않으면, 컴파일러는 추측하기를 거부하고 모호한 호출을 보고합니다. 교과서적인 사례는 각각 같은 등급의 변환이 필요한 두 오버로드입니다.
void f(int x);
void f(double x);
f(0L); // 오류: 모호함 - long -> int 와 long -> double 은 같은 등급의 변환
int도 double도 long에 대한 정확한 일치가 아니며, 두 변환이 같은 등급에 있으므로 호출이 모호합니다. 깔끔한 해결책이 두 가지 있습니다.
관련된 놀라움: 문자열 리터럴을 전달하는 경우입니다. void g(const string&)와 void g(bool)은 둘 다 g("hi")에 입찰하며, bool이 이길 수 있습니다. const char*가 std::string을 구성하는 것보다 더 적은 단계로 bool로 변환되기 때문입니다(널이 아니면 -> true). 문자열 리터럴이 알 수 없게 bool 오버로드를 호출하는 것을 보게 된다면 바로 이 이유 때문입니다. 정확한 일치를 가져가도록 const char* 또는 const string& 오버로드를 추가하거나, 인수를 원하는 타입으로 형변환하세요.
오버로딩과 기본 인수는 잘 어울리지 않는다
기본 인수는 오버로딩의 대체물이 아니며, 둘을 섞으면 모호성이 생깁니다. 각각이 같은 호출에 응답할 수 있어 컴파일러가 고를 수 없습니다.
void connect(string host, int port = 8080); // 인수 1개로 호출 가능
void connect(string host); // 이것도 인수 1개로 호출 가능
connect("localhost"); // 오류: 모호함 - 둘 다 단일 인수에 일치
호출 형태마다 하나의 접근 방식을 고르세요. 동작이 동일하고 단지 선택적 매개변수를 원할 뿐이라면 기본 인수를 사용하고, 서로 다른 인수 목록이 진정으로 다른 코드를 실행해야 한다면 오버로딩을 사용하세요. 두 시그니처가 같은 인수 개수에 대해 충돌하도록 섞는 것은 확실한 모호성 오류입니다.
확실히 못 박아 둘 만한 구분이 하나 더 있습니다. 오버로딩은 오버라이딩이 아닙니다. 오버로딩은 같은 스코프에서 같은 이름이지만 다른 매개변수를 가진 함수들 사이에서 컴파일 타임에 해석됩니다. 오버라이딩은 파생 클래스의 virtual 함수를 런타임에 대체하며 같은 시그니처를 요구합니다. 이는 나중에 가상 함수에서 다룰 주제입니다.
다음: 람다
오버로딩은 하나의 이름에 컴파일 타임에 선택되는 여러 타입의 구현을 부여합니다. 그러나 때로는 이름 있는 함수가 전혀 필요 없을 때도 있습니다. 사용하는 바로 그 자리에 정의하는, 종종 sort 같은 알고리즘에 넘기기 위한 작고 일회성인 함수가 필요할 때죠. 그것이 바로 람다입니다. 인라인으로 작성할 수 있고, 주변 변수를 캡처할 수 있으며, 단일 표현식으로 넘길 수 있는 익명 함수입니다. 다음에는 람다를 작성하는 방법과, 완전한 이름 있는 함수를 능가하는 때가 언제인지 살펴보겠습니다.
자주 묻는 질문
C++에서 함수 오버로딩이란 무엇인가요?
함수 오버로딩을 사용하면 매개변수 목록이 (개수, 타입, 순서 중 하나로) 다른 한, 같은 이름의 함수를 여러 개 정의할 수 있습니다. 컴파일러는 전달한 인수에 따라 어느 것을 호출할지 고르므로, print(42)와 print("hi")는 서로 다른 두 print 함수를 호출할 수 있습니다.
두 C++ 함수가 반환 타입만으로 구분될 수 있나요?
아니요. 오버로드는 매개변수 목록에서 달라야 합니다. int f(int)와 double f(int)는 컴파일 오류입니다. 반환 타입은 오버로딩 해석에 사용되는 시그니처의 일부가 아닙니다. 컴파일러는 반환값이 사용되기 전에, 호출 지점의 인수로부터 오버로드를 선택하기 때문입니다.
오버로드된 함수에서 "모호한 호출" 오류는 무엇 때문에 발생하나요?
두 오버로드가 똑같이 잘 일치해서 컴파일러가 하나를 선호할 수 없을 때 발생합니다. 전형적인 예는 f(int)와 f(double)을 f(0L)(long)로 호출하는 경우로, 둘 다 같은 등급의 변환이 필요합니다. 정확히 일치하는 오버로드를 추가하거나 인수를 원하는 타입으로 캐스팅하여 해결할 수 있습니다.