I'm FanJae.

[C++] Function III 본문

C++/Basic

[C++] Function III

FanJae 2024. 8. 8. 12:19

※ 본 포스트는 코드누리 C++ Basic 강의 내용을 보고 정리한 포스트입니다.

 

1. Suffix Return type(후위 반환 타입)

int add1(int a, int b) // Type A. 일반적인 함수
{
    return a+b;
}

auto add2(int a, int b) -> int // Type B. 반환 값인 int가 후위에 표기되어있다.
{
    return a+b;
}

- Type A가 지금까지 사용되었던, 일반적인 함수의 형태이다.

- C++11에서 부터 새롭게 등장한 함수의 표기법으로, 함수의 반환 타입을 함수의 () 뒤쪽에 적는 표기법이다.

※ 즉, 기존 함수의 표기법은 반환 타입이 입력보다 앞쪽에 표기되었던 반면, Suffix Return type은 반환 타입이 입력 뒤쪽에 표기된다. 이것을 반드시 사용해야 하는 경우는 복잡한 형태의 함수 템플릿을 만들거나, 람다 표현식을 쓸때 사용된다.

 

1-1. Suffix Return type이 반드시 필요한 예시

#include <iostream>

template <class T>
T add(T a, T b)
{
    return a+b;
}

int main()
{
    std::cout << add<int>(1, 2) << std::endl;  // OK
    std::cout << add(1,2) << std::endl;        // OK
    std::cout << add(1.2,2.1) << std::endl;    // OK
    
    //
    std::cout << add(1, 2.1) << endl; // 이는 인자를 보고 추론할 수 없음. 
}

- 위 3개와 다르게 add(1,2.1) 은 인자를 보고 추론할 수 없어서 오류가 발생한다.

- 이를 사용자가 명시해주는 방법도 존재한다.

std::cout << add<double>(1, 2.1) << std::endl

- 문제는 없으나, 함수 인자 타입이 명확할 때도, 명시해줘야 한다. 만약 두 개의 인자가 다른 타입이면, 타입을 명시 하기 위해 타입 변환 규칙을 모두 정확하게 이해해야 하여 비효율적이다. 그래서, 템플릿에서 서로 다른 타입을 받을 수 있도록 하는게 좋다.

 

<T1, T2를 서로 다른 Type으로 명시한 템플릿>

template<class T1, class T2>
? add(T1 a, T2 b)
{
   return a+b;
}

- 템플릿을 위와 같이 변환하면, 서로 다른 타입을 받는것은 가능하다.

- 하지만, ? 부분에 T1,T2를 적을수가 없다.

이때 사용하는 것이 decltype이다. decltype은 이미 포스트로 다룬 적이 있다. decltype 관련 포스트

 

<decltype을 반환 값으로 적은 예제> - Compile Error

template<class T1, class T2>
decltype(a + b) add(T1 a, T2 b)
{
   return a+b;
}

- 이와 같이 표기하고, 컴파일을 해도 에러가 발생한다. decltype이라는 키워드를 사용하는 시점에서 a와 b는 선언되지 않았기 때문이다. 따라서, 위와 같은 표기법 사용이 불가능하다. 

 

※ 하지만 함수라는 것은 입력 변수를 사용해서 출력을 표현하는 것이 당연히 가능해야한다. (함수라는 것은 입력에 특정한 계산이나 처리를 진행한 이후, 그 결과값을 반환하는 것이기 때문이다.) 그래서, 반환 타입을 후위에 명시하도록 하여, 반환 데이터 타입이 무엇인지 정확하게 할 수 있다.

 

<Suffix Return type을 이용한 예시> - C++11

template<class T1, class T2>
auto add(T1 a, T2 b) -> decltype(a + b)
{
   return a+b;
}

- auto를 통해서, 컴파일러는 함수의 반환 타입을 자동으로 결정하되, 복잡한 표현식의 결과로 결정될 때는 정확한 타입을 명시하도록 처리한 것이다.

template<class T1, class T2>
auto add(T1 a, T2 b) // C++14
{
   return a+b;
}

- C++14부터는 위와 같은 문법도 허용한다. 하지만, Template이 복잡해지는 경우나 사용자가 명확하게 하고 싶을때는 decltype을 명시해주는 것이 좋다. 


2. Delete Function

int gcd(int a, int b)
{
    return b != 0 ? gcd(b, a % b) : a;
}
int main()
{
    gcd(10, 4);
    
    gcd(3.3, 4.4); // 암시적 형변환으로 int로 바꿔서 실행됨.
}

- 위 예제는 최대 공약수를 구하는 것이다.

- 다만, 최대 공약수는 정수에만 있는 개념으로, 실수를 입력 받는 부분에 대해 에러로 처리할 수 있다.

double gcd(double a, double b); // 구현이 없이 선언만 넣는다.

- 컴파일러는 위를 보고, gcd에 대한 구현을 찾을 것이다.

- 컴파일러는 이 구현 정보가 외부 파일에 있을 수 있으니, 링커에게 넘긴다. 하지만 이 예제에서는 링커 에러가 발생한다.

 

※ 즉, 컴파일 에러가 발생하지 않는다. 이런 문제는 컴파일 타임에 에러를 내는것이 훨씬 낫다.

double gcd(double a, double b) = delete; // C++ 함수 삭제

- 이와 같이 처리하면, 컴파일러가 이를 인식하여, 삭제된 함수를 호출하려 한다는 컴파일 에러를 발생시킨다.

 

2-1. 정리

gcd(int, int) 함수가 존재할 때, 사용자가 gcd(3.3, 4.4); 로 사용할 경우 3가지의 케이스가 존재한다.

 

gcd(double, double) 함수에 대해서,

1. 제공하지 않으면, double이 int로 암시적 형 변환이 되어, gcd(int, int) 함수를 호출한다.

2. 선언만 제공하면, 링커가(double, double) 함수를 찾지 못해 Linker error가 발생한다.

3. 삭제(delete)한 경우 삭제된 함수를 사용할 수 없다는 Compile Error가 발생한다.

 

2-2. 언제 사용하는가?

C++에는 Class라는 문법이 있다. 여기서는 사용자가 만들지 않으면 컴파일러가 자동으로 생성하는 함수(복사 생성자)가 있다. 컴파일러에게 이러한 자동 생성하는 함수를 만들지 못하게 하고 싶을때 사용한다.

 

※ 복사 생성자에 대한 개념은 생성자 항목에서 다뤄질 예정이다.


3) Standard library

- C 표준 함수에는 printf, fopen, malloc, free 등과 같이 입출력, 메모리 할당/해지, 문자열 함수등을 제공한다.

- C++ 표준함수에는 C 표준 함수도 대부분 계속 사용 가능하고, 정렬, 선형검색, 이진검색, 순열 등 자료구조와 알고리즘 관련 함수도 제공한다.

- 즉, C++ 언어의 알고리즘이라는 것은 <algorithm> 헤더에 정의된 표준 함수를 의미한다.

 

3-1. C++ 알고리즘(표준함수)의 특징

- 인자로 알고리즘이 적용될 구간(first, last)을 전달한다.

#include <algorithm>
#include <iostream>
int main()
{
    int x[5] = {1, 3, 5, 2, 4};
    
    std::sort(x, x + 5);
    
    for(int i = 0; i < 5; i++)
    	std::cout << x[i] << std::endl;
}

- 정렬하고자 하는 구간의 시작과 끝 다음을 나타내는 반복자(주소)를 전달한다.

1 3 5 2 4
x x+1 x+2 x+3 x+4

 

여기서 정렬하고자 하는 시작 구간은 x이고, 끝 다음은 x+5이므로, sort(x,x+5)와 같이 전달하였다.

 

반복자(iterator)

- 요소를 가리키는 포인터 역활의 객체(변수)

- 추후 STL에 대한 내용을 포스트에서 다룰때 이야기 해보고자 한다.

 

3-2. C++20 constraint algorithm

- C++20 부터 제공되는 새로운 형태의 알고리즘

- 알고리즘(함수) 인자로 구간 뿐 아니라 컨테이너(배열이름)도 전달이 가능하다.

- std::ranges namespace 안에 제공

#include <algorithm>
int main()
{
    int x[5] = {1, 3, 5, 2, 4};
    
    std::ranges::sort(x, x+5);
    std::ranges::sort(x); // C++ 20부터 제공
}

 

※ 이 내용을 알기 위해서는 반복자, 컨테이너 등에 대한 개념의 이해가 필요하므로, 정렬을 하는 방법 정도만 알아두고, 반복자, 컨테이너는 STL 포스트에서 조금 더 알아보고자 한다.

 

4. Lambda expression

 

4-1. std::sort의 특징

- std::sort는 주어진 구간의 모든 요소를 오름차순(ascending)으로 정렬한다.

- std::sort를 사용해서 내림차순(descending)으로 정렬하려면, std::sort가 내부적으로 요소를 비교할 때 사용할 이항함수를 인자로 전달해야 한다.

- 단항 함수(unary function)는 인자가 1개인 함수, 이항 함수(binary function)은 인자가 2개인 함수를 의미한다.

#include <algorithm>
#include <iostream>

bool cmp(int a, int b) { return a > b; }

int main()
{
    int x[5] = {1, 3, 5, 2, 4};
    
    std::sort(x, x+5, cmp);
    for(int i = 0; i < 5; i++)
    	std::cout << x[i] << std::endl;
}

- 위와 같이 작업하면, 비교 함수를 통해 내림 차순을 하게 된다.

- std::sort 함수는 비교 함수가 반환하는 값을 사용해서 두 요소의 순서를 결정하기 때문에, true를 반환하면 a가 b보다 앞. 즉, 내림 차순으로 정렬하게 되는 것이다.

 

4-2. Lambda expression(람다 표현식) - C++11

#include <algorithm>
#include <iostream>

bool cmp(int a, int b) { return a > b; }

int main()
{
    int x[5] = {1, 3, 5, 2, 4};
    
    std::sort(x, x+5, [](int a, int b) { return a > b; } );
    for(int i = 0; i < 5; i++)
    	std::cout << x[i] << std::endl;
}

- 익명의 함수(객체)를 만드는 문법

- 함수이름(주소)가 필요한 위치에 함수 구현 자체를 표기할 수 있는 문법이다. 

- [] 기호가 람다표현식이 시작됨을 알리는 lambda introducer이다.

[](int a, int b) { return a > b; }

- 위와 같은 형태로 사용이 가능하다.

 

4-3. Lambda expression의 리턴 타입 표기

[](int a, int b) -> bool { return a>b;} );

- lambda expression의 리턴 타입 표기는 후위 반환 표기법(suffix return type)을 사용한다.

- 컴파일러가 return 문장을 보고 추론할 수 있는 경우, 리턴 타입은 생략이 가능하다.

 

4-4. Lambda expression의 활용

1. std::sort 등의 알고리즘(C++ 표준함수)의 인자로 사용한다.

2. auto 변수에 담아서 함수 처럼 사용한다.

auto add = [](int a, int b) { return a+b };

 

※ 일반 함수를 사용하지 않고 람다 표현식을 사용하는 이유는 특정 상황에서 일반 함수보다 빠르고, 지역변수를 캡쳐할 수 있는 능력이 존재한다. Lambda expression의 원리는 다소 복잡하여, 조금 더 상위 과정 포스트에서 별도로 다루고자 한다.

 

4-4-1. Capture local variable

#include <algorithm>
void foo(int n)
{
     if ( n < 40 )
          std::cout << n << " ";
}

int main()
{
    int score[5] = {95, 35, 63, 72, 22};
    
    std::for_each(score, score+5, foo);
}

- std::for_each는 구간의 모든 요소를 마지막 인자로 전달된 단항 함수에 차례대로 전달한다.

이 예제에서, pass라는 것을 main에서 입력받았다고 가정해보자.

int main()
{
    int score[5] = {95, 35, 63, 72, 22};
    int pass; 
    std::cin >> pass;
   
    std::for_each(score, score+5, foo);
}

- 이 경우, foo()에서는 pass를 전역 변수로 만들지 않으면, 해결 방법이 없다. pass를 인자로 전달하면 단항 함수가 아니기 때문이다. 이때 Lambda expression을 사용할 수 있다.

int main()
{
    int score[5] = {95, 35, 63, 72, 22};
    int pass; 
    std::cin >> pass;
   
    std::for_each(score, score+5, [pass] (int n) { if ( n < pass)
                                                   std::cout << n << endl;
                                                   } );
}

- 다소 복잡해보이지만, 이는 올바른 문법이다.

- Lambda expression 안에서는 자신이 속한 문맥(main 함수)의 지역변수에 접근이 가능하다.

- 이를 지역변수를 캡쳐(caputre)했다고 표현한다.

 

 

 

'C++ > Basic' 카테고리의 다른 글

[C++] 연산자와 제어문  (0) 2024.08.09
[C++] Function III  (0) 2024.08.08
[C++] Function II  (0) 2024.08.08
[C++] Function I  (0) 2024.08.07
[C++] C++ 에서 새롭게 추가된 Type & Variable  (0) 2024.08.06
Comments