함수 오버로딩
지난 장에서 함수를 정의하고 호출하는 방법을 통해 코드를 모듈화하고 재사용하는 중요성을 학습했습니다.
이번 절에서는 C++의 함수 이름 해석과 관련된 함수 오버로딩(Function Overloading)을 다룹니다.
함수 오버로딩은 같은 이름의 함수를 여러 개 정의할 수 있게 해주는 기능입니다.
언뜻 보기에는 혼란스러워 보일 수 있지만, 실제로는 매우 유용하며 코드의 가독성과 유연성을 크게 향상시킵니다.
함수 오버로딩이란 무엇인가?
함수 오버로딩은 하나의 함수 이름으로 서로 다른 매개변수 목록(Parameter List)을 가진 여러 함수를 정의하는 C++의 기능입니다.
컴파일러는 함수가 호출될 때 전달되는 인자의 타입과 개수에 따라 어떤 함수를 호출해야 할지 자동으로 판단합니다.
함수 오버로딩의 규칙: 함수를 오버로딩하려면 다음 조건 중 하나 이상을 만족해야 합니다.
주의: 함수의 반환 타입만 다른 것으로는 오버로딩할 수 없습니다. 컴파일러는 함수를 호출할 때 반환 타입 정보는 사용하지 않기 때문입니다.
int add(int a, int b) { return a + b; }
double add(int a, int b) { return (double)a + b; } // 컴파일 오류! 매개변수 목록이 동일함.오버로딩이 가능한지는 함수 이름이 아니라 매개변수 목록이 실제로 구분되는지로 판단합니다.
함수 오버로딩의 필요성
함수 오버로딩은 실제 프로그래밍에서 다음과 같은 상황에 유용하게 사용됩니다.
- 동일한 개념의 다른 데이터 타입 처리: 예를 들어, 두 수를 더하는 함수를 만들 때,
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 add(std::string, std::string) 호출
return 0;
}위 예시에서 add라는 동일한 이름의 함수가 4가지 버전으로 오버로딩되어 있습니다.
컴파일러는 main 함수에서 add를 호출할 때 전달되는 인자의 타입(int, double, 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;
}오버로드 해석의 모호성
오버로드 해석은 같은 이름의 후보 중에서 호출 가능한 함수를 남기고, 변환 비용을 비교해 하나의 최적 후보를 고르는 과정입니다.
컴파일러가 호출된 함수에 대해 어떤 오버로드된 함수를 선택해야 할지 명확하게 결정할 수 없는 경우를 모호성(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: int print(int) 호출
print(10.0); // OK: double print(double) 호출
// print(10L); // L은 long int 리터럴. int와 double 중 어느 것으로 자동 형 변환할지 모호!
// 컴파일러에 따라 경고 후 특정 함수 선택 또는 오류 발생
// 일반적으로 long은 int로도 double로도 변환 가능하므로 모호성 발생.
// 명시적으로 print((int)10L) 또는 print((double)10L) 처럼 캐스팅해야 함.
// print(true); // bool은 int로도 float/double로도 변환 가능하므로 모호성 발생!
// 컴파일 오류! (참고로, C++ 표준에 따르면 bool은 int로 변환하는 것이 더 나은 일치로 간주될 수 있습니다.)
return 0;
}이러한 모호성을 피하려면, 함수 호출 시 인자의 타입을 명확하게 지정하거나 (명시적 형 변환), 오버로딩된 함수들의 매개변수 목록을 서로 겹치지 않도록 설계해야 합니다.
main 함수는 오버로딩될 수 없다
main 함수는 프로그램의 시작점이며, C++ 표준에 의해 정의된 특별한 함수입니다. main 함수는 오버로딩될 수 없습니다. int main() 또는 int main(int argc, char* argv[])와 같은 특정 형태만 허용됩니다.
함수 오버로딩과 이름 맹글링
컴파일러는 오버로딩된 함수들을 구별하기 위해 내부적으로 함수 이름을 변경합니다.
이 과정을 이름 맹글링(Name Mangling) 또는 이름 장식(Name Decoration)이라고 합니다.
예를 들어 add(int, int)는 _Z3addii와 같이, add(double, double)은 _Z3adddd와 같이 내부적으로 다른 이름으로 변환됩니다 (실제 이름 맹글링 규칙은 컴파일러마다 다릅니다).
이렇게 변환된 이름에는 함수의 매개변수 타입 정보가 포함되어 있습니다.
그 덕에 컴파일러와 링커는 오버로딩된 함수들을 명확하게 구별하고 올바른 함수를 연결할 수 있습니다.
마지막으로 오버로딩을 설계할 때는 호출 형태가 겹치지 않는지, 반환 타입만으로 구분하려 하지 않는지 함께 점검해야 합니다. 아래 다이어그램은 안전한 오버로드 후보를 고르는 기준을 한 화면에 정리한 것입니다.