프로그래밍 언어/C & C++ 정리

C++ 함수 오버로딩과 C언어의 _Generic

뽀또치즈맛 2024. 11. 3. 12:30

 
 
C++ 함수 오버로딩은 C++만의 것이라고 보긴 어렵다.
C에서도 함수 오버로딩과 비슷한 기능이 있다.
 
_Generic 키워드를 사용하여 컴파일 시간에 인수의 형식을 기반으로
식을 선택하는 코드를 작성하면 된다.
인수의 형식을 호출할 함수가 선택되는 C++의 오버로딩과 유사하다.
여기서는 인수의 형식을 기반으로 평가할 식이 선택된다.
 
예를 들어 
_Generic(42, int : "integer", char : "character", default : "unknown");은
42의 형식을 평가하고 목록에서 일치하는 형식 int를 검색한다.
이 형식을 찾아 "integer"를 반환한다.
 
generic-selection: ( , )
 _Generic assignment-expressionassoc-list

assoc-list: ,
 association
 assoc-listassociation

association:
 type-name: :assignment-expression
 defaultassignment-expression
 
첫 번째 assignment-exptession을 제어 식이라고 한다.
제어 식의 형식은 컴파일 시간에 결정되고
assoc-list와 비교되어 평가 및 반환할 식을 찾는다.
제어 식은 평가되지 않는다.
예를 들어 _Generic(intFunc(), int : "integer", default : "error");은
런타임에 intFunc을 호출하지 않는다.
 
제어 식의 형식이 결정되면 const, valatile 및
restrict은 assoc-list와 비교하기 전에 제거된다.
 
이게 무슨 말이냐면
 
C에서 _Generic 키워드를 사용하여
함수 오버로딩과 유사한 기능을 구현하는 방법을 말하는 것이다.
 
1. _Generic 키워드를 사용하면 컴파일 시점에 변수나 표현식의 타입을 검사한다.
2 .해당 타입에 맞는 코드를 선택할 수 있다는 것이다.
 
이렇게 하면 C++의 함수 오버로딩과 비슷한 효과를 낼 수 있다는 것이다.
 

1. _Generic 키워드의 기본 사용법

_Generic을 사용하면 컴파일 시간에 인수의 타입을 평가하고,
그 타입에 따라 적절한 표현식을 선택할 수 있다.

_Generic(42, int : "integer", char : "character", default : "unknown");

 

2. 형식 지정과 선택의 동작 방식

_Generic의 구문은 다음과 같다.

_Generic(제어 식, 형식 : 결과, 형식 : 결과, ... , default : 기본값);

(제어 식은 타입을 결정하는 역할이다.)
 
그러면 assoc-list에 해당하는 형식 들은 뭘까? C++ 에서 매개변수와 비슷하다.
 
그럼 _Generic의 사용 제약 조건에 어떤 것이 들어갈까?
 

제약조건

  • assoc-list는 동일한 형식을 두 번 이상 지정할 수 없다.
  • assoc-list는 열거형의 기본 형식과 같이 서로 호환되는 형식을 지정할 수 없다.
  • 일반 선택에 기본값이 없는 경우 제어 식에는 일반 연결 목록에
    호환되는 형식 이름이 하나만 있어야 한다.

 
이렇게 말하면 잘 이해가 되지 않는다. 따라서 추가적으로 코드를 통한 비교를 해보자.
 

1. 같은 타입을 두 번 이상 지정할 수 없음

// 잘못된 예시: int를 두 번 지정
_Generic(42, int : "정수", int : "또 다른 정수", default : "기타");
// 컴파일 오류 발생

 
올바른 예시

_Generic(42, int : "정수", default : "기타");  // 올바른 사용

 

2. 기본값(default)이 없는 경우, 타입이 하나만 일치해야 함

잘못된 예시

int x = 5;

// 잘못된 예시: 기본값이 없고 일치하는 타입이 없음
_Generic(x, double : "실수");  // 컴파일 오류 발생

// 잘못된 예시: 기본값이 없고 일치하는 타입이 여러 개
_Generic(x, int : "정수", int : "또 다른 정수");  // 컴파일 오류 발생

 
올바른 예시

_Generic(x, int : "정수", default : "기타");  // 기본값 추가
_Generic(x, int : "정수");                   // 일치하는 타입 한 개

 
 

3. 호환되는 형식을 지정할 수 없음 (enum 예시 경우)

 
assoc-list 에 호환되는 타입을 함께 지정할 수 없다.
예를 들어 열거형 타입과 **열거형의 기본 타입 (ex.int)&**를
함께 지정하는 것은 불가능하다.
열거형은 내부적으로 정수형을 사용하더라도 다른 타입으로 간주되기 때문이다.
 

enum Color { RED, GREEN, BLUE };
enum Color color = RED;

// 잘못된 예시: enum Color와 int는 호환되므로 함께 지정 불가
_Generic(color, enum Color : "열거형", int : "정수", default : "기타");
// 컴파일 오류 발생

 
올바른 예시에는 열거형 타입을 하나만 지정해야 한다.

_Generic(color, enum Color : "열거형", default : "기타");  // 올바른 사용

 

제약 조건설명
같은 타입을 두 번 이상 지정할 수 없음assoc-list 에서 같은 타입을 중복해서 지정하면 안된다.
호환 되는 타입을 함께 지정할 수 없음열거형과 그 기본 타입 (int)처럼 호환되는 타입은 함께 지정할 수 없다.
기본 값이 없는 경우,
타입이 하나만 일치해야 함
default가 없으면 제어식과 일치하는 타입이 정확히 하나만 있어야 한다.

 
 
올바른 _Generic 예시

// Compile with /std:c11

#include <stdio.h>

/* Get a type name string for the argument x */
#define TYPE_NAME(X) _Generic((X), \
      int: "int", \
      char: "char", \
      double: "double", \
      default: "unknown")

int main()
{
    printf("Type name: %s\n", TYPE_NAME(42.42));

    // The following would result in a compile error because 
    // 42.4 is a double, doesn't match anything in the list, 
    // and there is no default.
    // _Generic(42.4, int: "integer", char: "character"));
}

/* Output:
Type name: double
*/

 
 
그럼 _Generic을 공부했으니
C++ 함수 오버로딩과 C _Generic의 차이를 요약해보자.
 
 

특징C++ 함수 오버로딩C _Generic
사용 방식함수 이름을 동일하게 하여
타입에 따라 호출을 자동 선택 해준다.
_Generic 키워드와
매크로를 사용해준다.
타입 검사 시점컴파일 시점에서 타입을 확인하고
적절한 함수를 선택한다.
컴파일 시점에
제어식의 타입을 확인한다.
다형성 구현 방식함수 오버로딩 직접 지원매크로와 _Generic으로 간접 구현
가독성과 유연성코드가 간결하고 가독성이 높음매크로와 _Generic으로 다소 복잡함
타입 안전성타입 안전성 보장타입 안전성이 떨어질 수 있다.

 
결론
C++에서 함수 오버로딩을 사용하여 더 가독성 높은 코드를 작성할 수 있지만,
C에서도 _Generic을 활용해 타입 기반 선택을 구현할 수 있다.
다만 _Generic은 제한적이기 때문에 C에서 함수 오버로딩과 비슷한 구현을 하려면
매크로를 사용해야 하며, 사용 시 코드가 복합하다는 것일 뿐이지 비슷하다.
 

// C

#include <stdio.h>

#define TYPE_NAME(x) _Generic((x), \
    int: "정수형", \
    double: "실수형", \
    char*: "문자열형", \
    default: "알 수 없는 형식" \
)

int main() {
    int a = 10;
    double b = 3.14;
    char* c = "Hello";

    printf("a는 %s입니다.\n", TYPE_NAME(a));    // 제어식: a, int 타입
    printf("b는 %s입니다.\n", TYPE_NAME(b));    // 제어식: b, double 타입
    printf("c는 %s입니다.\n", TYPE_NAME(c));    // 제어식: c, char* 타입

    return 0;
}


// C++
#include <iostream>
using namespace std;

void print(int x) {
    cout << "정수: " << x << endl;
}

void print(double x) {
    cout << "실수: " << x << endl;
}

int main() {
    print(10);      // int 타입 -> print(int) 호출
    print(3.14);    // double 타입 -> print(double) 호출
    return 0;
}

 
결정적 차이는
C++에서는 인수 타입이 함수 선택의 기준이며,
함수 오버로딩이 이를 자동으로 처리한다.
 
C에서는 제어식이 타입을 결정하는 역할을 하며,
_Generic을 통해 해당 타입에 맞는
표현식을 선택하는 방식으로 오버로딩 비슷한 효과를 낸다.
 
둘 다 공통점은 컴파일 타임에 바인딩 된다는 것이다.
 
 
그럼 이제 C++의 함수의 오버로딩을 복습한다는 의미로 간략히 알아보자.
 

메서드 오버로딩

 
C에서는 메서드 오버로딩이 없다
-> 멤버 함수라는 것이 없다.
멤버 함수라는 것이 없을 뿐이지 일반적인 오버로딩은 있다.(_Generic)
 
Java와 C++은 있다.
매개 변수가 다르면 호출 할 때
전달해 주는 인자에 따라 어떤 것이 호출할 지 결정된다.
 
함수 오버로딩이란?
 
매개변수 목록을 제외하고 모든 게 동일하지만
반환형은 상관이 없다.
 
어떤 것이 중복되지 않는 함수 시그니처일까?
 

void Print(int score);                    // (1)
void Print(const char* name);             // (2)
void Print(float gpa, const char* name);  // (3)
int Print(int score);                     // (4)
int Print(float gpa);                     // (5)

 
4번이다.
이미 매개변수 int형 1개 있는 함수는
이미 있는데 왜 또 이래... 싶은 것이다.
그래서 컴파일 에러가 발생한다 (컴파일 타임 바인딩되는 것이니까)
 

오버로딩 매칭하기

 
오버로딩 매칭이란?
오버로딩 된 함수 중에 어떤 함수를 호출해야 하는지 판단하는 과정이다.
 
함수 매칭 결과는 3개가 있다.

  • 매칭되는 함수는 찾을 수 없음  //컴파일 에러
  • 매칭되는 함수를 여러 개 찾음
    • 누굴 호출할지 모호 -> 컴파일 에러
  • 가장 적합한 함수 하나를 찾음  -> ok

 
 
 

'프로그래밍 언어 > C & C++ 정리' 카테고리의 다른 글

연산자 오버로딩을 남용하는 것  (1) 2024.11.10
연산자 오버로딩과 const  (0) 2024.11.06
복사 생성자  (4) 2024.11.02
구조체와 클래스의 차이  (1) 2024.11.02
new와 malloc의 차이  (1) 2024.11.01