함수 객체와 람다식
- 함수 객체 : 함수처럼 작동하는 객체 (function object/functor)
- 예제 코드를 보기 전에 알아야 하는 내용이 있다.
bool f(int x) {
//
}
int main() {
f(1);
}
- f(1)에서의 소괄호 ()는 연산자임을 기억하자.
- (물론 f는 객체가 아니긴 하지만) f.operator()(1); 이런 식으로 연산자를 호출해서 작동을 한다.
- 그래서 소괄호 연산자에 대해서도 오버로딩을 해줄 수가 있다.
#include <iostream>
using namespace std;
class Equals {
public:
Equals(int value) : value(value) {}
bool operator()(int x) const {
return x == value;
}
private:
int value;
};
int main() {
Equals eq(123);
cout << eq(12) << endl;
cout << eq(123) << endl;
}
- Equals eq(123)에서 소괄호 (123)은 소괄호 연산자가 아니라, 생성자이다.
- eq(12)는 Equals의 소괄호 연산자()를 호출하라는 의미이다
- 이 때 매개변수로 12를 받는다
- 함수 객체의 좋은점 : 같은 함수 객체로 여러가지 함수를 호출할 수 있다.
class Equals {
public:
Equals(int value) : value(value) {}
bool operator()(int x) const {
return x == value;
}
bool operator()(int x, int y) const {
return x + y == value
}
private:
int value;
};
int main() {
Equals eq(123);
cout << eq(12, 111) << endl;
cout << eq(123) << endl;
}
- 메인함수에서 같은 객체를 통해서 서로 다른 함수를 호출하고 있다.
- 함수 포인터랑 다른점 : 변수를 가지고 있다. (상태를 가지고 있다.)
int main() {
Equals eq(123), eq2(234);
cout << eq2(123) << endl; // false
cout << eq2(234) << endl; // true
}
함수 포인터를 함수 객체로 바꾸기
#include <iostream>
using namespace std;
class Func {
};
int square(int x) { return x * x; }
int myFunc(int x) { return x * (x - 15) / 2; }
int arrFnMin(const int arr[], int n, int (*f)(int)) {
int min = f(arr[0]);
for (int i = 1; i < n; i++) {
if (f(arr[i]) < min) {
min = f(arr[i]);
}
}
return min;
}
int main() {
int arr[7] = { 3, 1, -4, 1, 5, 9, -2};
cout << arrFnMin(arr, 7, square) << endl;
cout << arrFnMin(arr, 7, myFunc) << endl;
}
- 함수 포인터를 알고 있으니 int를 매개변수로 받아서 int를 리턴해주는 여러 함수를 Func이라는 하나의 클래스로 표현할 수 있을 것 같지만 불가능 하다
- square()에 해당하는 클래스와 myFunc()에 해당하는 클래스가 따로 있어야 한다
- 참고 : 아래 두 개 코드는 동일하게 작동한다.
class A {
public:
// 생략
} a;
class A {
public:
//생략
};
A a;
- square()와 myFunc()를 함수 객체로 만들어 주었다.
class Square {
public:
int operator()(int n) { return n * n; }
} square; // Square타입의 함수 객체 square 생성
class MyFunc {
public:
int operator()(int n) { return n * (n - 15) / 2; }
} myFunc; // MyFunc타입의 함수 객체 myFunc 생성
// 두 함수를 함수 객체로 만들어준다
// int square(int x) { return x * x; }
// int myFunc(int x) { return x * (x - 15) / 2; }
int arrFnMin(const int arr[], int n, int (*f)(int)) {
/* 생략 */
- 그리고 나서 보니, arrFnMin의 int (*f)(int) 자리에 어떤 것을 매개변수로 받아야 할지 고민이 될 것이다.
- Square를 넣어야 할까? MyFunc를 넣어야 할까?
- 기존에 알던 내용을 기반으로 생각해보면, Square와 MyFunc의 부모 클래스를 만들어서 오버라이딩 해주면 된다.
- 다른 방법에 대해서 배워보자
- functional 라이브러리를 이용해보자
#include <iostream>
#include <functional>
using namespace std;
/* 중략 */
int arrFnMin(const int arr[], int n, function<int(int)> f) {
int min = f(arr[0]);
for (int i = 1; i < n; i++) {
if (f(arr[i]) < min) {
min = f(arr[i]);
}
}
return min;
}
- square와 myFunc 모두 int를 매개변수로 받아 int를 반환해주고 있다.
- function<int(int)> f
- function<int(int)>인 이유는
- (int) : int를 매개변수로 받아
- int를 반환
- function<int(int)>인 이유는
- function<int(int)>타입인 f 객체는 int를 매개변수로 받고 int를 반환해주는 객체를 가리킬 수 있게 된다
- 여기에 치명적인 단점이 있다
- 성능(Performance)가 안좋아서 function이라는 클래스 템플릿은 성능상 사용하지 않는 것이 좋다
- 더 나은 해결책을 찾아보자 : template 이용
#include <iostream>
#include <functional>
using namespace std;
class Square {
public:
int operator()(int n) { return n * n; }
} square; // Square타입의 함수 객체 square 생성
class MyFunc {
public:
int operator()(int n) { return n * (n - 15) / 2; }
} myFunc; // MyFunc타입의 함수 객체 myFunc 생성
template<typename T>
int arrFnMin(const int arr[], int n, T f) {
int min = f(arr[0]);
for (int i = 1; i < n; i++) {
if (f(arr[i]) < min) {
min = f(arr[i]);
}
}
return min;
}
int main() {
int arr[7] = {3, 1, -4, 1, 5, 9, -2};
cout << arrFnMin(arr, 7, square) << endl;
cout << arrFnMin(arr, 7, myFunc) << endl;
}
- function<int(int)>를 T, 즉 f 자체의 임의의 타입으로 받겠다는 것이다
- arrFnMin(arr, 7, square)를 호출하면
- int arrFnMin(const int arr[], int n, T f)에서 T에 square가 들어간 함수가 생성되어 호출될 것이다.
- f의 타입은 Square가 된다.
- Square 클래스 안에 소괄호() 연산자가 존재하므로, f(arr[0]) 이런 것들이 가능해진다
- int arrFnMin(const int arr[], int n, T f)에서 T에 square가 들어간 함수가 생성되어 호출될 것이다.
- 장점
- 클래스 템플릿의 경우 모든 것이 컴파일 타임에 모든게 결정되므로 성능에 대한 걱정을 하지 않아도 된다. 효율적으로 작동할 것이다
- arrFnMin(arr, 7, square)에서 굳이 세 번째 매개변수에 함수 객체를 넘기지 않고 함수 포인터를 넘길 수도 있다
- 함수 포인터를 사용하는 경우에도 같은 템플릿을 사용해서 처리할 수 있다.
- 아직까지 불편한 점이 아직 남아있다
- 우리가 하고 싶은 것은 제곱(square)과 같이 특정 연산을 하는 함수를 arrFnMin 함수에 넘겨주고 싶었다
- n을 제곱해주는 함수라는 것을 알려주기 위해서 많은 작업을 하게 된다
- Square 라는 클래스를 만들고, 소괄호 연산자 오버로딩도 하고... 해야 한다
#include <iostream>
#include <functional>
using namespace std;
int square(int n) {return n * n;}
class MyFunc {
public:
int operator()(int n) { return n * (n - 15) / 2; }
} myFunc; // MyFunc타입의 함수 객체 myFunc 생성
template<typename T>
int arrFnMin(const int arr[], int n, T f) {
int min = f(arr[0]);
for (int i = 1; i < n; i++) {
if (f(arr[i]) < min) {
min = f(arr[i]);
}
}
return min;
}
int main() {
int arr[7] = {3, 1, -4, 1, 5, 9, -2};
cout << arrFnMin(arr, 7, square) << endl;
cout << arrFnMin(arr, 7, myFunc) << endl;
}
- 위에서 함수 포인터를 이용한 코드 동작 과정을 보면
- 먼저 위에서 square 함수가 어떤 함수인지(제곱을 리턴해 주는 함수)를 먼저 알려준 다음
- "이런 기능을 하는 함수가 있는데, 이름이 square 이다" 라고 선언하고
- cout << arrFnMin(arr, 7, square)에서 square를 넘겨주게 된다
- 먼저 위에서 square 함수가 어떤 함수인지(제곱을 리턴해 주는 함수)를 먼저 알려준 다음
- 우리는 arrFnMin(arr, 7, square) 여기에다가 이러한 기능을 하는 함수다라고 바로 알려주고 싶다.
- 그래서 등장한게 람다식(익명 함수)이다
- 람다식(익명 함수) : 이름이 없는 함수
- 여기서는 square라는 이름이 없고, 제곱하는 내용만 들어있는 함수
int main() {
int arr[7] = {3, 1, -4, 1, 5, 9, -2};
// 람다식
cout << arrFnMin(arr, 7, [](int n) -> int {return n * n;}) << endl;
cout << arrFnMin(arr, 7, myFunc) << endl;
}
- int 형 매개변수를 n을 받고
- (int n)
- int를 반환하는 함수이고
- -> int
- 함수의 내용물이 n * n을 리턴하는 함수이다.
- {return n * n;}
- 함수의 본문은 있지만, 이름이 없는 구조라는 걸 알 수 있다.
- myFunc도 람다식으로 바꿔서 코드를 완성해보자
#include <functional>
#include <iostream>
using namespace std;
template <typename T>
int arrFnMin(const int arr[], int n, T f) {
int min = f(arr[0]);
for (int i = 1; i < n; i++) {
if (f(arr[i]) < min) {
min = f(arr[i]);
}
}
return min;
}
int main() {
int arr[7] = {3, 1, -4, 1, 5, 9, -2};
cout << arrFnMin(arr, 7, [](int n) -> int { return n * n; }) << endl;
cout << arrFnMin(arr, 7, [](int n) -> int { return n * (n - 15) / 2; }) << endl;
}
- 람다식도 일반 함수처럼 타입이 존재하지만, 익명함수라서 밝혀주지 않는 것이다
- 람다식을 저장하고 싶은 경우가 생길텐데, 이 때 타입에 대한 정보를 알아야 한다
- ??? a = [](int n) -> int { return n * n; }
- 다행이도 캡쳐가 없는 람다식은 함수 포인터로 변환이 될 수 있다.
- 캡처 : 여기서 대괄호[] 안에 들어가는 것
- 람다식의 자세한 문법에 대해서는 아래에서 다룰게요.
int main() {
int arr[7] = {3, 1, -4, 1, 5, 9, -2};
int (*fp)(int) = [](int n) -> int { return n * n; };
cout << arrFnMin(arr, 7, [](int n) -> int { return n * n; }) << endl;
cout << arrFnMin(arr, 7, [](int n) -> int { return n * (n - 15) / 2; }) << endl;
}
- 캡처가 있을 때도 담고 싶으면, auto 라는 키워드를 사용할 수 있다.
- auto는 타입을 알아서 컴파일러가 추정할 수 있게 해준다
- ex) auto a = arr[1]이면 auto가 int로 지정된다
- 람다 표현식으로 런타임에 생성된 객체를 Closure(클로저)라고 한다
int main() {
int arr[7] = {3, 1, -4, 1, 5, 9, -2};
auto fp = [](int n) -> int { return n * n; };
cout << arrFnMin(arr, 7, [](int n) -> int { return n * n; }) << endl;
cout << arrFnMin(arr, 7, [](int n) -> int { return n * (n - 15) / 2; }) << endl;
}
다음과 같이 초기화 할 수도 있다.
auto f_11{
[](int a, int b) -> int {return a + b;}
};
참조) 두들낙서 C/C++ 강좌
람다식 문법
[ capture clause ] (parameters) -> return-type
{
definition of method
}
일반적으로 단순히 값을 반환하는 경우 return-type을 적지 않아도 컴파일러가 알아서 명시해준다
하지만 조건문 등을 통해서 return 값이 복잡하게 다뤄지는 경우 return-type을 명시해줘야 한다
캡쳐
람다식과 일반 함수와 다른점이라면,
일반 함수는 일반적으로 함수의 scope 외부에 존재하는 변수에 대해서 접근할 수 없다.(물론 참조형 매개변수를 사용하는 경우를 제외하고 말하는 것이다)
이와 다르게 람다식은 외부 변수에 대해 접근할 수 있다. 이를 외부 변수를 캡쳐한다고 말한다.
그리고 대괄호 [ capture clause ] 에서 어떻게 캡쳐를 할 것인지 정하게 된다
캡쳐 방식은 여러 개가 있다.
1. 아에 캡쳐를 하지 않는 경우
[]
2. 참조(Reference)로 모든 외부 변수를 캡쳐하는 경우
[&]
3. 값(value)로 모든 외부 변수를 캡쳐하는 경우
[=]
4. 특정 변수 A만 Reference로 캡쳐하는 경우
[&A]
5. 특정 변수 A만 Value로 캡쳐하는 경우
[A]
6. 특정 변수 A는 Reference로, 나머지는 변수는 Value로
[=, &A]
7. 특정 변수 A는 Value로, 나머지 변수는 Reference로
[&, A]
8. 특정 변수A는 Value로, 특정 변수 B는 Reference로, 나머지는 캡쳐 X
[A, &B]
9. 람다 표현식을 사용한 개체의 포인터를 가져온다
[this]
10. 람다 표현식을 사용한 개체 자체를 복사해서 가져온다(C++17)
[*this]
C++17까지는 =에서 this포인터를 암시적으로 가져왔지만, C++20부터는 this포인터를 명시적으로 가져와야 한다
람다 캡쳐 초기화
C++14부터 람다가 포함된 영역에 변수들이 존재해야 할 필요 없이 캡처 조항에 새로운 변수를 도입하고 초기화할 수 있다.
클로저가 생성될 때 캡쳐하므로, 람다 표현식을 여러 번 호출해도 값이 유지된다
'Programming Language > C++' 카테고리의 다른 글
[C++] 클래스와 함수의 friend 선언 (0) | 2024.06.29 |
---|---|
[C++] 스마트 포인터 (0) | 2024.06.28 |
[C/C++] 함수 포인터 (0) | 2024.06.28 |
[C++] 예외 처리(Exception handling) (0) | 2024.06.28 |
[C++] 템플릿 특수화와 비타입 파라미터 (0) | 2024.06.28 |