[C++] 호출 가능한 객체를 저장하는 std::function

반응형

std::function 객체

C++ 표준 라이브러리에서 제공하는 std::function 객체는 C++의 호출 가능한 모든 객체들( callable )을 저장하고 실행하는 객체입니다.
그리고, 여기서 말하는 callable은 () 연산자를 호출할 수 있는 객체를 말합니다.

예를 들어, 함수( function )는 함수명을 피연산자로 하는 () 연산자를 사용해서 호출할 수 있습니다.
그래서, callable이라고 할 수 있습니다.

int normal_func( int a, int b){		// 일반 함수
    return a + b;
}

int main(){

    int sum = normal_func( 3, 5 );	// callable
}

 

operator()를 구현한 함수 객체( function object )도 callable입니다.

struct funcObj{ // 함수 객체, callable
    
    int operator()( int a, int b){
        return a + b;
    }
};

int main(){

    funcObj sumObj;
    int res = sumObj( 10, 12 );     // 함수 객체 호출
}

위의 sumObj는 일반 함수를 호출하는 것과 같은 방법으로, 함수 객체operator()를 호출합니다.

 

[C++] 함수 객체( function object, functor ) : operator()를 구현한 타입

함수 객체( function Object ) 함수 객체( function object )는 operator() 함수를 구현한 타입( type )을 말합니다. C++ 표준 라이브러리에서는, 펑터( functor )라고도 불리는, 이 타입을 주로 컨테이너나 알고리즘

codingbonfire.tistory.com

 

그리고, 람다 표현식( lamda expression )으로 생성되는 클로저( closure )도 callable입니다.

auto lamda = []( int a, int b)->int{ return a + b; };
int res = lamda( 10, 12 );  // callable

람다 표현식에 관한 내용은 여기에서 볼 수 있습니다.

 

[C++] 간단한 함수 객체를 정의하기 위한 람다 표현식( lamda expression )

람다 표현식( lamda expression )줄여서 람다( lamda )라고도 하는 람다 표현식은 익명의 함수 객체를 정의하고 사용하기 위한 표기법입니다. 이 표현식은 간단한 기능을 구현하는데, 너무 많은 손이 가

codingbonfire.tistory.com

그리고, 이 같은 일반 함수나, 함수 객체, 람다 객체 등의 호출 가능한 객체들을, 한 가지 타입의 객체에 모두 저장하기 위해서, 특징을 추상화하여 작성된 클래스가 std::function입니다.
이 객체를 사용하면 얻을 수 있는 장점은, 함수의 주소를 저장하는 함수 포인터( function pointer )를 사용하며 얻을 수 있는 장점들을 그대로 가져올 수 있습니다.

 

이러한 장점들에서 가장 중요한 것은, 다른 객체나 함수에 이 std::function 객체를 전달할 수 있다는 것입니다.
이를 전달받은 객체나 함수는 작업 중 또는 작업 후에, 전달된 std::function 객체에 저장된 함수 객체를 호출해서( callback ) 자신의 목적을 달성할 수 있습니다.
그리고, 이러한 방식은 관련된 객체나 함수들을 모듈화 하고 재사용하기 수월하게 만듭니다.
또한, 전달되는 std::function객체가 저장하고 있는 기능(함수)을 바꿈으로써, 쉽게 동적인 변화를 가져올 수도 있습니다.


게다가, std::function은 일반 함수 외의 다른 타입( 함수 객체, 람다 객체 )까지도 저장할 수 있으므로, 함수 포인터를 확장한 기능이라고 할 수 있습니다.

 

std::function 객체 선언

std::function 객체를 사용하려면 먼저 헤더 파일을 포함해야 합니다.

#include <functional>

 

그리고, 선언은 다음과 같이 할 수 있습니다.

std::function< return_type ( parameter type... ) > function_name;

위의 반환 타입과 매개 변수의 타입은 모든 callable이 갖고 있는 특성입니다.
그리고, std::function은 이와 같은 타입 특성을 템플릿 매개변수로 가지는 클래스 템플릿( class template )입니다.

 

아래 예제는 일반 함수를 std::function 객체에 저장하는 예제입니다.

void int_func( int val ){}

void cstring_func1( const char* pstr ){}     // const char* 매개 변수

void string_func2( const string& str ){    // const stirng& 매개 변수
    std::cout << str << '\n';
}

int main(){

    function<void(int)> f = int_func;	// 함수를 저장
    
    function<void( const char*)> fcstr = cstring_func1;

    function<void( const char*)> fstr = string_func2;
    
    fstr("Hello !");	// "Hello !" 출력
}

이 예제를 보면, cstring_func1string_func2 함수를 같은 타입의 std::function 객체에 저장할 수 있음을 볼 수 있습니다.
이것이 가능한 이유는 std::function 객체에 전달된 인자가 암시적인 변환을 통해서 저장된 함수 객체에 전달될 수 있다면, 다른 타입의 매개 변수를 가진 함수라고 하더라도 저장할 수 있기 때문입니다.

 

좀 더 자세히 말하자면, 위에서 fstr("Hello !"); 문장을 통해서, std::function 객체인 fstrvoid operator()(const char*) 연산자를 호출하면, 이 연산자는 안에서, 저장된 함수 객체 string_func2를 다시 호출합니다.

그 과정에서, 컴파일러는 const char* 타입의 인자를 const std::string& 타입으로 암시적으로 변환할 수 있기 때문에, string_func2 함수를 fstr 객체에 저장하고 호출할 수 있게 되는 것입니다.

하지만, 함수 포인터( function pointer )는 반환 값의 타입과 매개 변수의 타입이 반드시 일치하는 함수만을 가리킬 수 있기 때문에, 이와 같은 마술을 부릴 수 없습니다.

 

다음 예제는 함수 객체( functor )와 람다 객체( closure )를 저장하는 예제입니다.

struct funcObj{ // 함수 객체
        
    int operator()( int a, int b){
        return a + b;
    }
};

int main(){

    function<int(int, int)> fObj1 = funcObj();   // 함수 객체를 저장

    auto lamda = []( int a, int b)->int{ return a + b; };
    function<int(int, int)> fObj2 = lamda;  // 람다를 저장
    
    int result = fObj2( 10, 12);    // result: 22. function 객체를 실행
}

 

타입 소거( type erasure )

std::fucntion를 사용하는 이유는 한 가지 방식으로 모든 타입의 함수 객체를 저장하고, 호출할 수 있기 때문입니다.

void print_func(int val){   // 일반 함수
    std::cout << "normal function: " << val << '\n';
}

struct print_functor{   // 함수 객체
    void operator()(int val){
        std::cout << "function object: " << val << '\n';
    }
};

// 모든 타입의 함수 객체를 전달하고, 호출하는 함수
void printAll( std::function< void(int) > printer, int val ){
    printer(val);
}

int main(){

    auto print_lamda{ // 클로저
        [](int val){ std::cout << "lamda: " << val << '\n'; } };   

    printAll( print_func, 3 );
    printAll( print_functor(), 5 );
    printAll( print_lamda, 7 );
}

▼출력

normal function: 3
function object: 5
lamda: 7

위의 printAll 함수는 모든 타입의 함수 객체를 전달받아 호출하는 함수입니다.

그리고, 이런 기능이 가능한 것은, 매개변수의 타입과 반환 타입만 맞다면, 함수 객체의 타입과 관계없이, 모든 함수 객체를 저장하고, 이 객체를 호출할 수 있는 std::function 객체가 있기 때문입니다.

 

이렇게, 하나의 인터페이스를 통해 다양한 구체적인 타입을 사용하는 보편적인 프로그래밍 기법을 타입 소거( type erasure )라고 합니다.

 

이 std::function 클래스 자체도 타입 소거의 훌륭한 예가 되지만, 다음과 같이, 이 std::function 클래스 객체를 사용해서 모든 타입의 객체를 출력할 수 있는 예를 만들 수도 있습니다.

class AllTypePrinter{
    
    std::function< void() > m_printer;  // 전달된 객체를 출력하는 객체
public:
    template< typename T >
    AllTypePrinter( T obj ) :
        m_printer{ [obj](){ std::cout << obj << '\n'; } }
    {
    }

    void print(){
        m_printer();
    }
};

int main(){

    AllTypePrinter printer1( 3.14 );
    AllTypePrinter printer2( "This is a string" );

    printer1.print();
    printer2.print();
}

▼출력

3.14
This is a string

AllTypePrinter 객체는 주어진 모든 타입( 실제로는 << 연산자를 구현한 타입만 가능 )을 출력할 수 있는 객체입니다.

이 객체의 기능이 가능한 것은 서로 다른 타입을 가지는 람다 함수라도 매개변수의 타입과 반환 타입만 같다면 저장할 수 있는 std::function 기능 덕택입니다.

( 참고로, 람다 함수의 타입은 다른 람다 함수의 타입과 다르며, 그 구체적인 타입은 컴파일러만이 알 수 있습니다. )

 

std::function 객체의 재설정

std::function 객체는 새로운 함수 객체를 대입하면, 이전의 함수 객체는 삭제되고, 새로운 함수 객체를 저장합니다.

void print_func(int val){   // 일반 함수
    std::cout << "normal function: " << val << '\n';
}

int main(){

    auto print_lamda{ // 클로저
        [](int val){ std::cout << "lamda: " << val << '\n'; } };   

    std::function< void(int) > fn{ print_func };	// 초기화
    
    if ( fn ){
        fn = print_lamda;	// 재설정
        fn( 7 );    // print_lamda
    }
    else{
        fn( 3 );    // print_function
    }
}

▼출력

lamda: 7

그리고, std::function 객체는 bool 타입의 판정이 필요한 곳( 예를 들면, ifwhile 구문 )에서, 저장하고 있는 함수 객체가 있는 경우 true로, 그렇지 않은 경우 false로 변환됩니다.

그래서, 위의 if ( fn )이 의미 있는 코드가 되는 것입니다.

 

만약, std::function 객체인 fn이 저장하고 있는 함수 객체를 지우고 싶다면 다음과 같이 할 수 있습니다.

void printState( const std::function<void(int)>& fn){
    if ( fn ){
        std::cout << "std::function has some function\n";
    }
    else{
        std::cout << "std::function has no function\n";
    }
}

int main(){

    auto print_lamda{ // 클로저
        [](int val){ std::cout << "lamda: " << val << '\n'; } };  

    std::function< void(int) > fn{ print_func };
    fn = print_lamda;

    fn = nullptr;	// 저장된 함수 제거
    printState( fn );
    
    fn = print_lamda;   // 재설정
    std::function< void(int) > fn2;
    fn2 = std::move(fn); // 다른 함수에 전달

    printState(fn);
}

▼출력

std::function has no function
std::function has no function

위와 같이, nullptr을 사용해서 기존의 저장된 함수를 제거하거나, 이동 연산( move semantics )을 사용해서, 다른 std::function 객체에 저장하고 있는 함수 객체를 이동시킬 수 있습니다.

 

멤버 함수( member function )의 저장

클래스의 멤버 함수를 호출하려면, 함수 호출에 기본적으로 필요한 함수명, 인자뿐 아니라, 그 클래스 타입의 객체를 지정해야 합니다.

#include <iostream>
using namespace std;

class CSomething{
    int m_nValue;

public:
    CSomething(int val) : m_nValue(val){}

    int add(int a, int b){		// 멤버 함수
        return m_nValue + a + b;	
    }
};

int main(){

    CSomething cObj(10);
    cout << cObj.add(2, 3) << endl;	// 멤버 함수 호출
}

그렇기 때문에, 위의 add 멤버 함수를 std::function 객체에 저장하려면 다음과 같은 매개변수를 가진 객체가 필요합니다.

std::function<int( CSomething&, int, int ) > f = &CSomething::add;

위에서, CSomething&와 같이 참조 타입을 사용한 것은, 저장된 함수를 호출할 때, 전달된 CSomething 타입의 객체가 복사되는 것을 막기 위해서입니다.

그리고, add 함수가 CSomething 멤버 함수라는 것을 나타내야 하므로, 객체 f에는 add 앞에 클래스명과 범위 지정 연산자 ::를 이용한 CSomething::add를 저장해야 합니다.

int main(){

    CSomething cObj(10);
        
    std::function<int(CSomething, int,int)> f = CSomething::add;
    cout << f( cObj, 2, 3) << endl; 
}

그리고, 위와 같이 std::function에 저장된 함수를 호출하기 위해서, CSomething 타입의 객체를 다른 인자와 함께 전달해야 됩니다.

 

 

정리

std::function 객체는 C++의 호출 가능한 모든 객체들( 일반 함수, 함수 객체, 람다 객체 )을 저장하고, 호출할 수 있는 객체입니다.

std::function는 함수 포인터( function pointer )의 모든 장점을 가진, 확장된 기능입니다.

std::function를 사용해서 타입에 무관한( type erasure ) 포괄적인 코드를 작성할 수 있습니다.

 

 

이 글과 관련 있는 글들

함수를 전달하기 위한 함수 포인터( function pointer )

클래스의 멤버 함수를 가리키는 멤버 함수 포인터

null 포인터와 nullptr 리터럴( literal )

 

 

 

 

  • 네이버 블로그 공유
  • 네이버 밴드 공유
  • 페이스북 공유
  • 카카오스토리 공유