[C/Objective-C] 함수 포인터와 코드 블럭

함수포인터와 코드블럭

함수포인터나 코드블럭은 (실제로 코드 블럭을 더 많이 보게 되겠지만) 알고나면 별개 아닌데, 코드상에서의 그 괴기한(?) 모습 때문에 보는 사람으로 하여금 등골이 오싹해지도록 하는 신비한 힘이 있다. 오늘은 코드블럭과 함수포인터에 대해 살짝 이야기해보고자 한다.

콜백

콜백은 다른 코드로 넘겨지는 실행가능한 코드의 조각을 뜻한다. 특정한 함수를 호출하여 그 작업이 완료된 후에 함수가 호출되도록 하여 적절한 응답을 받고 동시에 후속 작업의 호출이 외부에서 진행될 수 있도록 한다. 콜백은 넘겨진 직후 즉시 실행되는 동기 콜백이 있고, 시간이 흐른 후에 호출되는 비동기 콜백이 있다. 콜백은 언어마다 형태가 다른데 서브루틴, 람다, 블럭이나 함수 포인터 등으로 구현된다.

여기서 주목해야 할 점은 함수의 인자값으로 객체나 특정 타입의 값을 넘기는 것 외에 실행 가능한 코드를 전달하는 것이다. 예를 들어 상황에 따라 다른 함수를 호출해야 하는 경우에 조건 분기를 사용할 수도 있지만, 이런 분기가 너무 다양하다면 코드의 가독성을 위해서는 실행 가능한 코드의 경우들을 모두 배열 같은 것으로 만들고, 특정 조건에 따라 그 중 하나가 실행되도록 디자인하면 좋을 것이다.

뜬금없이 콜백에 대한 이야기를 하는 것은 C에서는 이 콜백이 함수 포인터를 통해서 이루어지기 때문이다. Objective-C는 코드 블럭을 더 많이 사용하는데, C의 이러한 문법들은 여전히 모두 사용가능하다.

함수 포인터

C에서 배열의 이름은 묵시적으로 배열이 시작하는 첫 번지의 포인터와 같은 주소를 나타내는 포인터로 간주할 수 있다. (엄밀하게 둘은 다르다. sizeof() 매크로 함수연산자를 써서 확인해보라) 마찬가지로 함수의 이름은 메모리상의 함수의 주소를 가리킨다. 그리고 이러한 함수의 주소를 저장할 수 있는 함수 포인터도 당연히 존재한다. 함수 포인터의 선언방법은 조금 특이한데 다음과 같다.

리턴타입 (*함수포인터 변수이름)(파라미터1타입, 파라미터2타입…);

실제 코드는 다음과 같이 표현되는데, 만약 정수형 파라미터를 2개 받고 역시 정수형을 리턴하는 함수에 대한 포인터는 다음과 같이 쓴다.

int (*myCalc)(int, int);

이렇게 선언한 함수포인터는 따로 선언한 함수의 이름을 그대로 대입하면 된다. 단 이때 대입하는 함수는 반환형과 인자의 개수 및 각 인자의 타입이 일치해야 한다.

다음 코드는 간단한 함수 포인터 예제이다.

// simple function pointer
#include <stdio.h>

int addNumbers(int a, int b)
{
    return a+b;
}

int main(void) 
{
    int (*myfunc)(int, int) = addNumbers;
    // 이제 myfunc는 addNumber와 같은 주소를 가리키게 된다. 
    printf("%i\n", myfunc(2,3));
    return 0;
}
// output : 5

여기서는 함수 포인터 변수를 만들고 여기에 함수를 대입한 다음, 일반 함수인것처럼 이를 호출했다. 이는 마치 함수 포인터를 함수의 다른 이름인 것처럼 쓸 수 있게 한다. 단순히 함수의 다른 이름을 만들었다기보다는, 함수를 변수에 대입해서 옮겨다닐 수 있도록 했다는데 의의가 더 크다고 볼 수 있다. 이렇게 함수 포인터를 사용해서 함수를 포터블하게 만들면 다음과 같은 방식의 코드도 가능해진다.

// function pointer as arguement of another function
#include <stdio.h>

int myAdd(int a, int b){ return a+b; }
int myMultiply(int a, int b){ return a*b; }
int myCalc( int (*op)(int, int), int a, int b) { return op(a, b); }

int main(void) 
{
    printf("%i\n", myCalc(myAdd, 2, 3));
    printf("%i\n", myCalc(myMultiply, 2, 3));
    return 0;
}

// output : 
// 5
// 6

위 예제에서는 함수 포인터를 인자로 받는 함수 myCalc를 만들고, 다른 인자를 이 함수에 전달해서 결과를 리턴하도록 했다. 메인 함수에서는 각각 2, 3의 정수에 대해 넘겨주는 함수 포인터(함수의 이름은 함수의 주소와 같다고 보므로)에 따라 다른 연산을 수행하도록 했다.

함수 포인터는 함수의 위치를 담는 포인터 변수이므로 그 크기는 컴파일 시점에 정해지게 된다. 그렇다는 것은, 이 함수 포인터 변수는 구조체의 멤버가 될 수 있다는 것이다. 따라서 다음과 같은 코드를 통해 구조체의 멤버로 함수를 지정하는 것이 가능하고, 실제로 제대로 컴파일 된다.

// Function Pointer in structure
#include <stdio.h>

struct obj {
    int a;
    int b;
    int (*sum)(int, int);
};

int addNumbers (int a, int b) { return a+b; }

int main(void)
{
    struct obj myObj = {2, 3};
    myObj.sum = addNumbers;
    printf("%i\n", myObj.sum(myObj.a, myObj.b));

    return 0;
}

// output:
// 5

코드 블럭

코드 블럭은 최근 C표준에 합류한 기능으로 Objective-C 2.0에서부터 도입되었고 Xcode 4로 넘어오면서 구현이 가능해졌다. 함수 포인터를 사용하면 함수에 대해 콜백 함수를 전달해서 나중에 실행되게 할 수 있는데, 이렇게 하려면 넘겨주려는 코드를 미리 함수로 선언해둬야 하는 불편함이 따른다. 코드 블럭은 함수처럼 선언하지 않더라도 중괄호로 싸여진 블럭 자체를 객체로 보고 이를 함수의 인자로 넘길 수 있게 해준다. 문법은 함수 포인터와 동일한데, 포인터를 의미하는 * 연산자 대신에 ^를 사용한다.

리턴타입 (^코드블럭이름)(파라미터타입1, 파라미터타입2…);

동작하는 가장 간단한 예는 다음과 같다. (컴파일은 clang으로 objective-C 파일로 컴파일 했다.)

#include <stdio.h>

int multiplier = 8;
int (^myBlock)(int) = ^(int num) { return num * multiplier; }

int main(void) 
{
    printf("%i\n", myBlock(10));
    return 0;
}
// output:
// 80

이렇게 하는거나 함수를 따로 선언하는 거나 무슨 차이가 있냐고? 그럼 위의 코드를 약간 라인의 위치만 바꿔보자.

#include <stdio.h>


int main(void) 
{
    int multiplier = 8;
    int (^myBlock)(int) = ^(int num) { return num * multiplier; }
    printf("%i\n", myBlock(10));
    return 0;
}
// output:
// 80

코드 블럭을 선언하고 정의하는 라인을 메인함수의 내부로 옮겼고 이 역시 제대로 동작한다. 이는 함수포인터와 별반 차이가 없어 보일 수 있는데, 함수의 내부에서 또 다른 일회용 함수를 정의한 것과 마찬가지의 효과를 얻을 수 있는 것이다! 그럼 코드 블럭을 인자로 받는 함수의 예를 들어보도록 하자.

#include <stdio.h>

int calc(int (^op)(int, int), int a, int b) { return op(a, b); }

int main(void)
{
    printf("%i\n", calc(^(int a, int b){ return a+b; }, 2, 3));
    printf("%i\n", calc(^(int a, int b){ return a*b; }, 2, 3));
    return 0;
}

앞서 작성한 함수 포인터 예제를 코드 블럭으로 변경했다. 차이가 있다면 인자로 넘길 함수를 따로 정의하지 않고, calc 함수 호출 시점에 코드 블럭을 통해 로직을 바로 작성해서 넘겨줬다는 점이다. 다소 코드가 난해해 보일 수 있지만, 여기까지 읽어오는 데 큰 무리가 없었다면 코드의 양이 줄어서 한결 간결했졌다고 느낄 수 있을 것이다.

코드 블럭 역시 함수 포인터와 마찬가지로 컴파일 시점에 그 크기를 알 수 있다. 따라서 이 역시 구조체나 공용체 속에 들어갈 수 있으며, 정상적으로 컴파일 된다.

struct obj {
    int a;
    int b;
    int (^add)(int, int);
};

int main(void)
{
    struct obj myObj = {1,2};
    myObj.add = ^(int a, int b){ return a+b; };
    printf("%i\n", myObj.add(myObj.a, myObj.b));
    return 0;
}

그 외에 것들

타입지정

함수 포인터나 코드블럭은 그 자체로 사용자 정의 타입으로 만들 수도 있다. 예를 들자면..

typedef int (*myFunction)(int, int) myFunction ;
typedef int (^myCodeBlock)(int, int) myCodeBlock ;

myFunction aFP = someFunction;
myCodeBlock aBlock = ^(int a, int b){ return (a > b) ? a : b; };

와 같은 식으로 사용자 정의형으로 선언하고, 그 형에 따르는 변수를 선언하여 사용할 수도 있다.

특히 Objective-C에서 블럭은 원시 클래스와도 같다. Objective-C의 블럭은 copy도 가능하고, 리테인, 릴리즈가 가능하며 심지어 블럭포인터는 ARC의 관리를 받을 수도 있다. 심지어 다음 코드는 문법에 완전히 맞으며 제대로 컴파일도 되고 실행도된다.


        #import 
        typedef void(^xs_block_t)(void);
        int main(int argc, char const *argv[])
        {
            @autoreleasepool{
                xs_block_t b = [^(){NSLog(@"abcdefg");} copy];
                b();
                [b autorelease];
            }
            return 0;
        }

컴파일

이 글의 모든 예제는 GNUSTEP, mingw32, clang 을 사용하여 32비트 윈도우 환경에서 컴파일하였다. 코드 블럭을 사용하기 위해서는 컴파일러 옵션 중 -fblocks를 주고 컴파일 하면 된다.

  • JSS

    Block 문법 공부중이었는데 너무 좋은 자료 잘 보고 갑니다.

  • kyejusung

    와우!! 정말 머리에 쏙쏙 들어오네요. 좋은 지식 공유 감사합니다!