(C/Objc) 함수 포인터와 코드 블럭

C언어에서 어떤 타입의 배열과 포인터는 다른 것이지만, 흔히들 혼용해서 사용하기도 한다. 이것은 배열의 이름 그자체가 배열의 시작번지를 나타내는 값으로 작동하기 때문이다. 그런데 함수에 대해서도 이와 비슷한 규칙이 성립한다. 프로그램이 실행되는 동안, 프로그램 내의 모든 실행코드들은 메모리에 올라와 있다. 이 말은 즉, 함수가 실행되기 위해서는 해당 함수의 실행코드가 메모리 상에 로드되어 있다는 말이고, 함수의 시작번지를 프로그램은 어떤식으로든 알고 있다는 것이다. 배열의 예와 마찬가지로 함수의 이름은 함수의 실행코드가 로드되어 있는 메모리 주소를 가리킨다.

그렇다면 임의의 T타입 배열의 이름을 T타입 포인터에 대입해서 그 참조를 이리저리 가지고 다닐 수 있는 것처럼, 함수 역시 어떤 포인터에 대입해서 사용할 수 있지 않을까? 그렇다면 ‘함수의 포인터’란 어떻게 정의할 수 있을까?

일반적인 값의 타입에 대해 포인터는 T *의 형태로 표기할 수 있다. 함수의 포인터를 만들기 위해서는 조금 특별한 문법이 사용된다.

사실, C에서 타입이라는 것은 그 타입의 값이 점유하는 메모리의 크기를 구분하기 위한 용도가 가장 주된 의의인데, 함수 타입의 크기 자체가 인자나 리턴값에 따라서 달라질 것 같지는 않다. 하지만 예외적으로 함수 포인터의 타입에는 리턴 및 인자들의 타입을 포함할 필요가 있는데, 이는 함수 포인터를 통해서 해당 함수를 호출하는 경우가 있을 수 있기 때문이다.

이러한 특성 때문에 함수 포인터는 다음과 같은 포맷으로 작성하게 된다.

리턴타입 (*포인터이름) (인자타입1, 인자타입2, 인자타입3)

ex) 두 수를 받아 연산하여 리턴하는 함수에 대한 포인터

int (*operate)(int, int) 
float (*operate_f)(float, float)

이렇게 정의된 함수 포인터에 함수 이름 대입할 수 있으며, 호출시에는 함수와 동일하게 사용가능하다. 간단한 예를 살펴보자. 타입 시그니쳐가 동일한 두 함수 add, mul 에 대해서 함수 포인터를 사용하여 연산을 호출하는 예이다.

#include <stdio.h>

int add(int x, int y) { return x + y; }

int mul(int x, int y) { return x * y; }

int main(void) {
    int (*op)(int, int) = add;
    int x = 3, y = 4;
    printf("%d + %d = %d\n", x, y, op(x, y));
    op = mul;
    printf("%d * %d = %d\n", x, y, op(x, y));
    return 0;
}

사실 C에서는 함수포인터를 이런 방식으로는 그닥 유용하게 쓸 일이 없다. 그럼 언제 유용하게 쓸 수 있을까? 가장 기본적이고 흔한 사용방법은 “함수를 인자로 받는” 함수를 작성할 때이다. 함수를 인자로 넘겨받는 함수는 흔히 정렬을 구현할 때 사용할 수 있다. int, float의 배열인 경우에는 배열의 원소들을 간단히 < 연산자로 대소를 비교하면 되는데, 구조체나 불투명 타입인 경우에는 직접 비교를 하기가 어렵다. 이 때에는 두 값의 전후관계를 알려줄 함수를 함께 인자로 받으면 임의의 타입에 대한 정렬을 구현할 수 있다.

다음 함수의 원형은 임의의 opaque 타입 배열을 정렬하는 함수이다. int (*isbefore)(void*, void*)는 두 개의 void * 형 포인터를 받아 int를 리턴하는 함수의 포인터로 두 원소의 전후를 비교할 수 있는 함수를 의미한다.

void sort_array(void ** array, size_t length, int (*isbefore)(void*, void*));

함수포인터가 인자에 들어있는 함수를 호출할 때에는 그 인자의 자리에 함수 이름을 넣어서 전달해주면 된다.

함수 포인터는 크기가 고정된 포인터이므로 구조체의 멤버로도 추가될 수 있다. 물론 C에서는 다른 OOP언어들과 같이 객체와 메소드를 묶는 기능을 제공하지는 않지만, 메소드 비슷한 것을 흉내낼 수는 있다.


이처럼 함수 포인터를 사용하면 함수에 대한 참조를 이리 저리 전달하거나 함수의 인자 혹은 리턴값으로도 사용할 수 있는 등, 활용할 수 있는 곳이 많이 있다. 그럼에도 불구하고 함수 포인터는 별도로 선언 및 정의한 함수만을 사용할 수 있다는 단점이 있다.

블럭

블럭(혹은 코드 블럭이라고도 함)은 C 표준은 아니고, 애플에 의해서 (Objective-C 2.0 에서 추가된 기능이다.) 추가된 확장으로 익명함수 혹은 클로저라 불리는 기능을 도입한 것이다. clang 컴파일러에 -fblocks 옵션을 추가해서 사용할 수 있으며, 별도의 런타임 라이브러리를 필요로 한다. 이 라이브러리는 기본적으로 (당연히) 애플의 OS에는 통합되어 있으며, 다른 플랫폼에서도 설치가 가능하다.

블럭은 ‘실행 가능한 코드 조각’을 객체화한 것으로 함수 포인터와 비슷하다고 할 수 있는데, 특이하게 블럭이 정의된 곳의 지역 변수들을 캡쳐할 수 있다. (보통 다른 언어들의 클로저가 이런 기능을 가지고 있다.) 그리고 해당 코드의 정의 자체가 표현식으로 취급되기 때문에 외부에 미리 정의해놓지 않은 코드를 생성할 수 있다는 장점이 있다.

코드 블럭을 선언 및 정의하는 방법은 함수 포인터와 비슷하다. * 대신에 ^를 쓴다.

리턴타입 (^블럭이름)(파라미터타입, 파라미터타입,...)

간단히, 블럭을 인자로 받는 함수를 정의하고 이를 호출하는 방법을 보이는 예를 들어보자.

#include <stdio.h>

int apply(int x, int(^bk)(int)) {
    return bk(x);
}

int main(void) 
{
  int a = 10;
  int b = 100;
  int(^block)(void) = ^{ return a * 3 + b; };
  
  b = 40;
  int y = apply(a, ^int(int x){
      return x * 3 + b;
  });
  
  printf("%d, %d\n", y, block());
  return 0;
}

먼저 함수 apply()는 정수 값과 “정수를 받아 정수를 리턴하는” 블럭을 인자로 받아서, 블럭에 정수값을 적용한 결과를 리턴한다.

각 변수의 초기값이 주어졌을 때, block은 그 시점의 값을 캡쳐했다. 아직 실행되지는 않은 상태로 존재한다.

변수 b의 값을 변경한 후 apply()를 호출한다. 내부 식은 block과 동일한 식을 사용했다. b의 값이 변경됐기 때문에 이 결과인 y는 70이다.

마지막 출력부분에서 block()을 실행하는데, 여기에는 캡쳐 시점의 값이 그대로 유지되고 있으므로 130으로 계산된다.

코드 블럭 역시 함수 포인터와 마찬가지로 컴파일 시점에 그 크기를 알 수 있다. (포인터와 같이 고정 크기이다.) 이 말은 코드 블럭이 구조체 속으로 들어갈 수 있다는 것이다. __block 키워드를 앞에 선언하면, 블럭에서 참조할 때 캡쳐(복사)되는 것이 아니라 블럭과 메모리를 공유하도록 선언할 수 있다. 이 방법을 사용하면 구조체에 블럭을 멤버로 할당하여 마치 연결된 메소드처럼 사용하는 것도 가능하다.

https://developer.apple.com/library/archive/documentation/Cocoa/Conceptual/ProgrammingWithObjectiveC/WorkingwithBlocks/WorkingwithBlocks.html#//apple_ref/doc/uid/TP40011210-CH8-SW3

#include <stdio.h>

// 코드블럭으로 타입 정의
typedef int(^IntProperty)(void);

struct obj_z {
    int x;
    int y;
    IntProperty add;
};

int main(void) {
    __block struct obj_z z = { 1, 2 };
    // z는 __block을 써서 선언했으므로
    // 캡쳐 대신 메모리 공유로 접근한다.
    z.add = ^{ 
        return z.x + z.y;
    };
    z.x = 10, z.y = 20;
    printf("%d, %d, %d\n", z.x, z.y, z.add());
    // -> 10, 20, 30 이 출력
    return 0;
}

보너스 – 타입을 정의할 때

함수 포인터나 코드블럭은 그 자체로 사용자 정의 타입으로 만들 수 있다. 이 두 타입에 대한 타입 정의 문법은 조금 특별한 모양이다.

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

객체로서의 블럭

Objective-C에서 블럭은 원시 클래스와도 같다. Objective-C의 블럭은리테인, 릴리즈가 가능하며 심지어 ARC의 관리를 받을 수도 있다.

컴파일 방법

블럭 기능을 사용할 때, macOS플랫폼에서는 그냥 하면 된다. 윈도에서는 MSYS2를 설치하고, MSYS2 상에서 clang, llvm, BlocksRuntime 패키지를 설치한 다음, -fblocks 옵션을 주고 컴파일한다.

만약 컴파일이 제대로 되지 않고 링커 에러가 나는 경우에는 다음과 같이 2단계로 나눠서 컴파일하면 된다.

$ clang -S -fblocks sample.c -o sample.s
$ clang sample.s -o sample.exe -lBlocksRuntime