함수 오버로딩
같은 이름의 함수를 매개변수 목록으로 구분하는 오버로딩 규칙과 컴파일러의 호출 후보 선택 과정을 이해합니다.
지난 장에서 함수를 정의하고 호출하는 방법을 통해 코드를 모듈화하고 재사용하는 중요성을 학습했습니다.
이번 절에서는 C++의 함수 이름 해석과 관련된 함수 오버로딩(Function Overloading)을 다룹니다.
함수 오버로딩은 같은 이름의 함수를 여러 개 정의할 수 있게 해주는 기능입니다.
언뜻 보기에는 혼란스러워 보일 수 있지만, 실제로는 매우 유용하며 코드의 가독성과 유연성을 크게 향상시킵니다.
함수 오버로딩이란 무엇인가?
함수 오버로딩은 하나의 함수 이름으로 서로 구분되는 매개변수 타입 목록(Parameter-Type-List)을 가진 여러 함수를 선언·정의하는 C++의 기능입니다.
일반 함수 호출에서는 이름 조회로 찾은 후보들 가운데 인자 개수와 타입, 허용되는 변환을 검사해 가장 적합한 함수를 선택합니다.
함수 오버로딩의 규칙: 함수를 오버로딩하려면 다음 조건 중 하나 이상을 만족해야 합니다.
주의: 일반 함수는 반환 타입만 다르게 해서 오버로딩할 수 없습니다.
같은 이름과 같은 매개변수 타입 목록을 가진 선언은 반환 타입만 바꿔 별도 후보가 될 수 없으며, 일반 함수 호출의 기대 결과 타입도 이 둘을 구분하는 기준이 되지 않습니다.
int add(int a, int b) { return a + b; }
double add(int a, int b) { return (double)a + b; } // 컴파일 오류! 매개변수 목록이 동일함.매개변수 이름만 바꾸어도 새 오버로드가 되지 않습니다. 값 매개변수의 최상위 const는 함수 타입을 만들 때 제거되므로 void read(int)와 void read(const int)도 같은 함수입니다. 배열·함수 타입 매개변수는 각각 대응하는 포인터 타입으로 조정되므로 void scan(char[])와 void scan(char*)는 구분되지 않습니다. 마찬가지로 using Callback = int(int);일 때 void apply(Callback)과 void apply(Callback*)도 같은 함수 선언입니다. 반면 int&와 const int&처럼 참조 대상 타입이 다르면 별도 오버로드가 될 수 있습니다.
오버로딩이 가능한지는 이름이나 매개변수 이름이 아니라, 조정 뒤의 매개변수 타입 목록이 실제로 구분되는지로 판단합니다.
Identity before resolution
새 오버로드가 되려면 조정된 매개변수 타입 목록이 달라야 합니다. 호출문에서 보이지 않는 매개변수 이름이나 반환 타입만으로는 일반 함수를 구분할 수 없습니다.
개수 차이 · 가능
add(int, int)와add(int, int, int)는 매개변수 타입 목록의 길이가 다릅니다.타입 차이 · 가능
add(int, int)와add(double, double)은 대응하는 매개변수 타입이 다릅니다.서로 다른 타입의 순서 · 가능
show(std::string, int)와show(int, std::string)은 순서를 바꾼 조정된 매개변수 타입 목록이 다릅니다.매개변수 이름만 · 불가
show(int left)와show(int value)처럼 이름만 바꿔도 함수 타입은 달라지지 않습니다.값 매개변수의 최상위 cv · 불가
read(int)와read(const int)은 함수 타입을 만들 때 최상위const가 제거되므로 같은 함수입니다.배열·함수 매개변수 조정 · 불가
scan(char[])과scan(char*)은 같은 매개변수 타입입니다.using Callback = int(int);일 때apply(Callback)과apply(Callback*)도 같습니다. 배열·함수 타입 매개변수는 각각 대응하는char*와Callback*로 조정됩니다.반환 타입만 · 불가
int add(int, int)와double add(int, int)처럼 같은 매개변수 타입 목록을 반환 타입만으로 나눌 수 없습니다.
이름은 하나의 의미 계열을 약속한다
정수·실수 덧셈처럼 같은 작업을 다른 입력 타입에 제공할 때 이름을 공유합니다. 문자열 결합도 API가 같은 “결합” 개념으로 정의할 때만 add 계열에 둡니다.
구분 가능해도 모호한 호출은 만들 수 있다
오버로드를 늘릴수록 후보 집합도 커집니다. 0, nullptr, 문자 리터럴의 변환 대상과 기본 인자를 함께 점검하고, 의미가 다르거나 호출 형태가 겹치면 별도 이름을 사용합니다.
설계 순서: 선언 identity를 먼저 확인한 뒤, 실제 호출마다 어떤 후보와 변환이 생기는지 별도로 검토합니다.
함수 오버로딩의 필요성
함수 오버로딩은 실제 프로그래밍에서 다음과 같은 상황에 유용하게 사용됩니다.
- 동일한 개념의 다른 데이터 타입 처리: 예를 들어, 두 수를 더하는 함수를 만들 때,
int형 정수뿐만 아니라double형 실수도 더할 수 있게 하고 싶을 수 있습니다. 오버로딩이 없다면addInt,addDouble과 같이 별개의 함수 이름을 사용해야 합니다. - 다양한 인자 개수 지원: 어떤 작업을 수행하는 데 인자가 한두 개만 필요할 수도 있고, 더 많은 정보가 필요할 수도 있습니다.
- 코드의 일관성 및 가독성 향상: 동일한 작업을 수행하는 함수에 같은 이름을 부여함으로써 코드의 의미론적인 일관성을 유지하고 가독성을 높일 수 있습니다. 사용자 입장에서는
add라는 이름만으로도 덧셈 기능을 떠올릴 수 있습니다.
함수 오버로딩 예시
정수형 덧셈과 실수형 덧셈을 모두 처리하는 add 함수를 오버로딩해 봅시다.
#include <iostream>
#include <string>
// 1. 두 정수를 더하는 함수
int add(int a, int b) {
std::cout << "int add(int, int) 호출" << std::endl;
return a + b;
}
// 2. 두 실수를 더하는 함수 (매개변수 타입 다름)
double add(double a, double b) {
std::cout << "double add(double, double) 호출" << std::endl;
return a + b;
}
// 3. 세 정수를 더하는 함수 (매개변수 개수 다름)
int add(int a, int b, int c) {
std::cout << "int add(int, int, int) 호출" << std::endl;
return a + b + c;
}
// 4. 문자열을 결합하는 함수 (같은 '결합' 개념으로 이름을 공유)
std::string add(std::string s1, std::string s2) {
std::cout << "std::string add(std::string, std::string) 호출" << std::endl;
return s1 + s2;
}
int main() {
// 호출 시 인자의 타입과 개수에 따라 적절한 함수가 선택됩니다.
std::cout << "결과: " << add(5, 7) << std::endl; // int add(int, int) 호출
std::cout << "결과: " << add(3.5, 2.1) << std::endl; // double add(double, double) 호출
std::cout << "결과: " << add(1, 2, 3) << std::endl; // int add(int, int, int) 호출
std::cout << "결과: " << add("Hello", "World") << std::endl; // 문자열 리터럴에서 std::string 매개변수 구성
return 0;
}위 예시에서 add라는 동일한 이름의 함수가 4가지 버전으로 오버로딩되어 있습니다.
컴파일러는 main 함수에서 add를 호출할 때 인자 개수와 실제 타입, 필요한 변환을 비교하여 가장 적절한 함수 버전을 찾습니다. "Hello"와 "World"의 타입은 std::string이 아니라 const char 배열입니다. 이 호출에서는 배열이 포인터로 변환되고 std::string의 변환 생성자가 사용되는 사용자 정의 변환을 거쳐 문자열 후보가 호출 가능해집니다. 이는 std::string 인자의 정확한 일치와는 다릅니다.
이것을 함수 오버로드 해석(Function Overload Resolution)이라고 합니다.
매개변수 타입의 순서를 바꿔 조정된 매개변수 타입 목록이 달라지면 오버로딩이 가능합니다.
#include <iostream>
#include <string>
// 문자열과 정수를 출력하는 함수
void printValue(std::string text, int number) {
std::cout << "텍스트: " << text << ", 숫자: " << number << std::endl;
}
// 정수와 문자열을 출력하는 함수 (매개변수 순서 다름)
void printValue(int number, std::string text) {
std::cout << "숫자: " << number << ", 텍스트: " << text << std::endl;
}
int main() {
printValue("Age", 30); // void printValue(std::string, int) 호출
printValue(100, "Score"); // void printValue(int, std::string) 호출
return 0;
}오버로드 해석의 모호성
오버로드 해석은 호출 지점의 이름 조회로 후보를 만들고, 호출 가능한 함수만 남긴 뒤, 각 인자에 필요한 암시적 변환 순위를 비교해 하나의 최적 후보를 고르는 과정입니다. 단순히 모든 인자의 비용을 숫자로 더하는 방식이 아닙니다. 기본적인 표준 변환 순위는 정확한 일치 → 승격(promotion) → 변환(conversion) 순이며, 표준 변환 순서는 사용자 정의 변환 순서보다 우선합니다. 먼저 한 후보가 다른 후보보다 모든 인자에서 나쁘지 않고 적어도 하나에서 더 좋은지 비교하고, 변환 순서가 동률이면 나머지 tie-break 규칙을 적용합니다. 호출 가능한 후보가 하나도 없으면 일치하는 함수가 없는 오류이고, 복수의 호출 가능한 후보 사이에 유일한 최적 후보가 없으면 모호한 호출입니다.
Lookup → viable → rank → best
오버로드 해석은 변환 비용을 모두 더하는 계산이 아닙니다. 호출 지점에서 찾은 후보를 걸러낸 뒤 각 인자의 변환 순서를 비교하여 유일한 최적 후보를 찾아야 합니다.
-
이름 조회로 후보 집합을 만든다
호출 지점에서 조회되는 같은 이름의 선언들이 후보가 됩니다. 프로그램 전체의 같은 이름을 무조건 모두 모으는 것은 아닙니다.
-
호출 가능한 함수만 남긴다
인자 개수, 기본 인자, 각 인자에서 매개변수 타입으로 갈 수 있는 암시적 변환을 검사합니다.
-
각 인자의 변환 순위를 비교한다
기본 표준 변환 순위는
exact match가promotion보다,promotion이conversion보다 좋고, 표준 변환 순서는 사용자 정의 변환 순서보다 우선합니다. -
유일한 최적 후보를 결정한다
먼저 모든 인자에서 나쁘지 않고 적어도 하나에서 더 좋은 후보를 찾습니다. 변환 순서가 동률이면 나머지 tie-break 규칙을 적용합니다. 호출 가능한 후보가 없거나 유일한 최적 후보가 없으면 오버로드 해석에 실패합니다.
정수 두 개
add(5, 7)은 두 인자가 정확히 일치하는add(int, int)를 선택합니다.실수 두 개
add(3.5, 2.1)은 두 인자가 정확히 일치하는add(double, double)을 선택합니다.정수 세 개
add(1, 2, 3)은 인자 개수가 맞는 세 매개변수 후보를 선택합니다.문자열 리터럴
add("Hello", "World")은const char[N]에서std::string으로 가는 사용자 정의 변환을 거쳐 문자열 후보를 선택합니다. 정확한 일치는 아닙니다.long· 모호함print(10L)은long에서int와double로 가는 변환이 모두 conversion 순위이므로 유일한 최적 후보가 없어 컴파일 오류입니다.bool· 정수 선택print(true)는bool에서int로 가는 promotion이double로 가는 conversion보다 좋아 정수 후보를 선택합니다.
오버로드 해석 성공
모든 비교와 tie-break 뒤 하나의 최적 후보가 남습니다. 다만 선택된 함수가 삭제됐거나 접근할 수 없는 등 후속 검사를 통과하지 못하면 최종 호출은 여전히 ill-formed입니다.
컴파일 오류
호출 가능한 후보가 없거나 최적 후보가 둘 이상이면 컴파일러가 임의로 하나를 선택하지 않습니다.
핵심: 선언 identity와 호출 해석은 별개입니다. 함께 선언할 수 있어도 특정 인자에서는 모호해질 수 있습니다.
컴파일러가 호출된 함수에 대해 어떤 오버로드된 함수를 선택해야 할지 명확하게 결정할 수 없는 경우를 모호성(Ambiguity)이라고 합니다.
이런 경우 컴파일 오류가 발생합니다.
모호성이 발생하는 흔한 경우- 자동 형 변환(Implicit Type Conversion) 규칙으로 인해 여러 함수가 일치하는 경우: 예를 들어
int와float매개변수를 가진 함수가 오버로딩되어 있는데,double값을 인자로 전달하면int로 변환할지float로 변환할지 모호해질 수 있습니다. - 기본 인자(Default Arguments)와 오버로딩이 함께 사용될 때: 기본 인자를 가진 함수와 다른 오버로딩된 함수가 동일한 호출 형태를 만들 수 있는 경우.
#include <iostream>
void print(int value) {
std::cout << "Int: " << value << std::endl;
}
void print(double value) {
std::cout << "Double: " << value << std::endl;
}
int main() {
print(10); // OK: print(int) 호출
print(10.0); // OK: print(double) 호출
// print(10L); // 컴파일 오류: long->int와 long->double이 모두 conversion 순위라 모호함
print(static_cast<int>(10L)); // OK: 호출 의도를 명시해 print(int) 선택
print(true); // OK: bool->int promotion이 bool->double conversion보다 우선
return 0;
}print(10L)은 표준상 반드시 모호한 컴파일 오류이며 컴파일러가 임의로 하나를 선택하지 않습니다. 반대로 print(true)는 bool에서 int로의 승격이 double로의 변환보다 좋은 변환 순위이므로 print(int)를 선택합니다.
이러한 모호성을 피하려면 함수 호출 시 static_cast로 의도를 명확하게 지정하거나, 오버로드들의 매개변수 목록과 기본 인자가 같은 호출 형태를 만들지 않도록 설계해야 합니다. 기본 인자는 함수 타입을 구분하지 않습니다. 빠진 후행 인자를 보완할 수 있는 후보를 호출 가능하게 만들지만, 실제로 주어진 인자에 대한 변환 순위에는 참여하지 않으며 함수가 선택된 뒤 빠진 인자를 초기화할 때 사용됩니다.
main 함수는 오버로딩될 수 없다
Hosted 프로그램에는 전역 범위의 main 함수가 정확히 하나 있어야 하므로 오버로딩할 수 없습니다. Freestanding 환경에서 main 정의가 필요한지는 구현 정의입니다.
main의 타입은 그 밖에는 구현 정의이지만, 구현체는 int main()과 int main(int argc, char** argv)에 해당하는 두 형태를 모두 허용해야 합니다. 추가 매개변수를 가진 구현 정의 형태를 허용할 수도 있지만 반환 타입은 int이며, 서로 다른 main 오버로드를 함께 선언할 수는 없습니다.
함수 오버로딩과 이름 맹글링
별도 컴파일을 사용하는 많은 C++ 구현은 링커 심볼에서 오버로드를 구별하기 위해 함수 이름과 타입 정보를 인코딩합니다. 이를 이름 맹글링(Name Mangling) 또는 이름 장식(Name Decoration)이라고 부릅니다.
예를 들어 Itanium C++ ABI 계열에서 전역 비템플릿 함수 add(int, int)는 _Z3addii, add(double, double)은 _Z3adddd처럼 표현될 수 있습니다. 이 경우 일반 비템플릿 함수 이름에는 반환 타입이 인코딩되지 않습니다. MSVC 등 다른 ABI는 다른 규칙을 사용하며, C++ 표준은 특정 맹글링 형식을 요구하지 않습니다.
따라서 이름 맹글링은 오버로딩의 언어 규칙 자체가 아니라, 컴파일러와 링커가 그 결과를 구현하는 흔한 ABI 전략으로 이해해야 합니다.
마지막으로 오버로딩을 설계할 때는 호출 형태가 겹치지 않는지, 반환 타입만으로 구분하려 하지 않는지 함께 점검해야 합니다.
위 두 다이어그램처럼 먼저 선언 가능한 오버로드 집합인지 확인하고, 실제 호출에서는 후보·호출 가능성·인자별 변환 순위·유일한 최적 후보의 순서로 점검합니다.