본문 바로가기

진리는어디에/C++

[C++] 람다와 함수 포인터

들어가며

이전 포스트(람다(lambda) 표현식)에서는 람다 표현식을 auto 변수에 담아 마치 함수 포인터인터인것 처럼 사용했었다.

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

std::cout << f1(10, 20) << std::endl;

위 예제 처럼 람다를 auto 변수에 담는 것 말고도 함수 포인터를 이용해 담아 사용할 수도 있다. 이번 포스트에서는 람다 표현식의 함수 포인터로의 변환을 살펴 보도록 한다.

람다 -> 함수 포인터

앞에서 람다 표현식을 auto 변수 뿐만 아니라 함수 포인터에도 담는 것이 가능하다고 했다. 예제를 작성해 보자면 아래와 같다.

int (*f2)(int, int) = [](int a, int b) { return a + b; };

std::cout << f2(10, 20) << std::endl;

위와 같이 람다 표현식을 동일한 형식의 함수 포인터에 대입하면 컴파일에서 함수 호출까지 정상적으로 진행되는 것을 확인할 수 있다.

하지만 람다 -> 함수 포인터의 형변환에서 제약 사항이 있는데, '캡쳐를 사용하지 않는 람다 표현식만 함수 포인터로 사용할 수 있다'라는 것이다. 왜 이런 제약 사항이 발생하는지는 알아도 딱히 도움 안되고 몰라도 살아가는데 어려움이 없는 정보이므로 부록에서 이어 추가 설명하도록 하겠다. 관심 있으신 분들은 부록을 이어서 읽도록 하자.

마치며

다소 짧은 내용이지만 중요한것은 '람다를 함수 포인터 처럼 사용할 수 있다. 단, 캡처를 사용한 람다는 변환 불가하다'로 정리 된다. 아래 부터 설명 되는 부록은 알면 좋지만 몰라도 크게 상관 없는 보다 부가적인 내용을 담고 있으니 관심이 있다면 읽어 보도록 하자.

다음 포스트에서는 단일 코드로 다양한 인자에 대응 할 수 있는 제네릭 람다 표현식에 대해 살펴 보도록 하겠다.

부록 1. 보다 깊은 람다 -> 함수 포인터

우리는 앞서 람다 표현식은 '이름 없는 함수 객체'라고 배웠다. 그런데 어떻게 객체가 함수 포인터에 저장 될 수 있는 것일까?

이유는 컴파일러에 의해 생성되는 함수자 클래스 내부에서 함수 포인터로 암시적 형변환을 지원하고 있기 때문이다.

  • 컴파일러가 만드는 클래스에 '함수 포인터로 변환 될 수 있는 변환 연산자 함수 제공'
// int (*f)(int, int) = [](int a, int b) { return a + b; };

class __CompilerGeneratedName
{
public :
    inline int operator()(int a, int b) const
    {
        return a + b;
    }
    
    using FP = int(*)(int, int);
    // typedef int(*FP)(int, int); // 위와 동일
    operator FP () const
    {
        return &__CompilerGeneratedName::operator(); //
    }
};

int int (*f)(int, int) = __CompilerGeneratedName{};
                      // __CompilerGeneratedName{}.operator int(*)(int, int)와 동일

위 예제에서 11라인을 보면 using을 이용해 함수 포인터 타입을 FP라는 이름으로 변환 하고 있다. 이유는 형 변환 오퍼레이터를 오버로드 하는 과정에서 함수 포인터 타입 자체를 사용하면 '사용자 정의 변환에 인수를 사용 할 수 없다'라는 컴파일러 오류를 발생 시키기 때문이다.

형 변환 오퍼레이터 안에서 함수 포인터를 리턴해 주도록 하면 암시적인 형변환이 완료되지만 아직 문제는 남아 있다. 위에서 리턴하는 operator()는 멤버 함수이기 때문에 일반 함수 포인터와는 호환이 되지 않는다. 일반 함수 포인터와 클래스 멤버 함수가 호환이 되기 위해서는 static 멤버 함수여야 한다. 하지만 괄호 연산자 함수는 static 함수가 될 수 없으므로 아래 처럼 괄호 연산자 함수와 동일한 작업을 하는 다른 staitc 함수를 추가 한다.

// int (*f)(int, int) = [](int a, int b) { return a + b; };

class __CompilerGeneratedName
{
public :
    inline int operator()(int a, int b) const
    {
        return a + b;
    }
    
    static inline _invoke(int a, int b) const // operator()와 동일한 일을 하는 static 함수
    {
        return a + b;
    }
    
    using FP = int(*)(int, int);
    // typedef int(*FP)(int, int); // 위와 동일
    operator FP () const
    {
        return &__CompilerGeneratedName::operator(); //
    }
};

int int (*f)(int, int) = __CompilerGeneratedName{};
                      // __CompilerGeneratedName{}.operator int(*)(int, int)와 동일

operator()와 동일하게 한다고 함수의 const 한정자도 복사해 왔지만 static 함수는 const 한정자를 사용할 수 없다. 그리고고 이제 형 변환 연산자에서 리턴하는 함수 포인터를 operator()에서 _invoke로 변경해주면 끝이다.

class __CompilerGeneratedName
{
public:
    inline int operator()(int a, int b) const
    {
        return a + b;
    }
    
    static int _invoke(int a, int b) // static함수는 const 한정자 사용 불가
    {
        return a + b;
    }
    
    using FP = int(*)(int, int);    //typedef int(*FP)(int, int); 동일
    operator FP () const
    {
        return &__CompilerGeneratedName::_invoke; // static 함수 포인터 리턴
    }
};

이상 람다 표현식이 함수 포인터로 변환 되기 위해서 어떠한 일이 필요한지 살펴 보았다.

이제는 캡처를 사용하는 람다 표현식은 왜 함수 포인터로 변환될 수 없는지에 대한 이유를 살펴 보자.

int v1 = 10;

int (*f1)(int, int) = [  ] (int a, int b) { return a + b; }; // ok
int (*f2)(int, int) = [v1] (int a, int b) { return a + b + v1; }; // error

위 코드는 람다 표현식을 함수 포인터에 담고 있다. 첫번째 f1의 경우에는 별 문제가 없으나 f2의 경우에는 'lambda []int (int a, int b)->int"에서 "int (*)(int, int)"(으)로의 적절한 변환 함수가 없습니다" 라는 컴파일 에러를 발생 시킨다.

앞에서 우리는 람다에서 캡쳐를 사용하면 컴파일러가 생성하는 클래스에 캡처하는 변수와 매칭되는 멤버 변수와 멤버 변수를 초기화할 수 있도록 생성자가 만들어진다는 것을 배웠다.

class __CompilerGeneratedName
{
    int v1;
public:
    __CompilerGeneratedName(int& v1) : v1(v1) {}
    
    inline int operator() (int a, int b) const { return a + b + v1; }
    
    static int _invoke(int a, int b) { return a + b + v1; } // error
    
    // ... 생략 ...
};

위의 경우 괄호 연산자 함수에서 v1을 사용하지만 이는 멤버 함수가 멤버 변수를 사용하는 것이므로 아무런 문제가 되지 않는다. 하지만 9라인의 static 함수 _invoke의 경우에는 static 함수에서 클래스의 멤버 변수를 접근하려고 하므로 컴파일 에러가 발생한다.

즉, 람다 표현식이 캡처를 하는 순간 일반 함수 포인터로 변환 되어야 하는 static 멤버 함수를 생성하지 못하므로 '캡처를 사용한 람다 표현식은 함수포인터로 변환이 불가능 하다'

부록 2. 같이 읽으면 좋은 글

유익한 글이었다면 공감(❤) 버튼 꾹!! 추가 문의 사항은 댓글로!!