c언어에서 고차함수 프로그래밍기법

ozon1000의 이미지

함수형 언어의 가장 큰 장점은 여러가지가 있겠지만, 가장 도드라진 특징은 모든 계산과 식들이 함수로 이루어져 있다는 것이지요.

그래서 프로그램에서 함수를 한번 정의하면 재사용이 쉽고 또한 함수합성(고차함수)프로그래밍이 가능하여, 비교적 프로그램이 간결하고 짧아지는 장점이 있습니다.

c언에서 비슷한 기법을 사용할 수 없을까? 라는 의문을 품고 관련 논문을 찾아보던 도중에 함수 포인터를 이용하면 가능하다는 글을 읽고 Test중인데...

이해안가는 코드가 있네요.. 한번도 c언어를 사용하면서 이렇게 사용한적은 없어서요!

형님들! 코드 설명좀 부탁드립니다.

참고로 main문 안의 g = test1(); 에서는 문법오류가 생기지만 정상적으로 build는 되며 실행됩니다.
실행되면 콘솔창에 22라고 뜨게되지요.
제생각에는 원리도 간단한데... main문안의 내용이 이해가 잘 가지 않습니다.
부탁드립니다.!

#include <stdio.h>
 
void *test1();
void test2();
 
int main()
{
	void(*g)(void);
	g = test1();
	g();
}
 
void *test1()
{
	return test2;
}
 
void test2()
{
	printf("22\n");
}
<\code>

익명 사용자의 이미지

#include <stdio.h>
 
typedef void (*pvoidfun)(void); // 적절한 함수 포인터 typedef
 
// void *test1();
pvoidfun test1(void);           // test1의 반환형은 "매개변수 없고 반환형 없는 함수의 포인터", 즉 void (*)(void)입니다.
                                // 근데 함수의 반환형으로 함수 포인터를 직접 걸 수는 없네요.
                                // 그거 알아보느라 C언어 표준까지 들춰봤는데 방법이 없는 듯.
                                // 혹시 방법 아시는 분 있으면 알려주세요.
                                // 그건 그렇고, 매개변수가 없는 함수 선언에는 void를 넣어줍시다.
// void test2();
void test2(void);               // 이하동문.
 
// int main()
int main(void)                  // 이하동문. 이건 함수 정의라서 생략해도 되긴 하지만...
{
    void(*g)(void);             // 또는 pvoidfun g;
    g = test1();
    g();
}
 
// void *test1()
pvoidfun test1(void)            // 반환형 바로잡습니다.
{
    return test2;
}
 
// void test2()
void test2(void)
{
    printf("22\n");
}

이렇게 고치면 컴파일러 에러 옵션을 아무리 민감하게 설정해도 아무 문제 없을겁니다.
원래 코드에서는 함수 포인터를 암시적으로 void *로 캐스팅하고, 그걸 또 다시 함수 포인터로 암시적 캐스팅하고 있었거든요.
아무리 함수 포인터가 직접 다루기에는 타이핑하기 귀찮은 물건이라고는 하지만, 어쨌거나 합법적으로 할 방법이 있긴 있는겁니다.

C언어의 함수 규칙은 재귀적이고 유연하기 때문에, 마음만 먹으면 이상(李箱) 작가님의 시 마냥 "함수포인터를반환하는함수의포인터를반환하는함수의포인터를반환하는함수" 같은 걸 문제 없이 사용할 수 있습니다.

물론 정 C언어로 시를 쓰고 싶은 생각이 아니라면, 혹은 ioccc(http://www.ioccc.org/) 같은 데 출품할 작품을 만들고 계시는 게 아니라면 그런 코드는 최대한 지양하는 편이 바람직하지요. 설령 어지간한 이유가 있다고 한들, 가독성 및 유지보수 측면에서 그런 코드는 점수가 마이너스거든요.

익명 사용자의 이미지

C언어의 함수 규칙은 재귀적이고 유연하기 때문에
-> C언어의 "타입" 규칙은 재귀적이고 유연하기 때문에.

바로잡는 김에 조금 더 덧붙이자면, C언어에서 함수 타입과 함수 포인터 타입 사이에는 미묘한 문법적 허용이 있습니다.
올려드린 코드를 주의깊게 보신다면 아마 눈치채셨을 텐데, 솔직히 저도 코드 고치는 동안 깜빡해버려서 뒤늦게 덧붙이네요.

1)
test1의 반환값과 반환 타입을 보세요. test2를 반환하고 있는데, 이건 엄밀히 말하면 함수 포인터가 아니라 함수 타입이죠.
엄밀히 따지면 &test2와 같이 주소를 취해서 반환해야 합니다.

근데 그냥 test2라고만 써도 알아서 포인터로 변환됩니다.

2)
main에서 함수 포인터 g를 호출하는 부분도 잘 보시길.
g는 엄밀히 말해 함수가 아니라 함수 포인터이고, 따라서 엄밀히 따지면 함수 호출 연산자 ()가 바로 붙을 수 없었어야 하죠.
게다가 후위 연산자가 전위 연산자보다 먼저 결합한다는 규칙에 의해, 엄밀히 따지면 (*g)();처럼 썼어야 했어요.

하지만 g();와 같이 함수 포인터에다 대고 함수 호출 연산자를 적용해도 알아서 호출됩니다.

이 두 미묘한 점은 마치 배열과 포인터 사이의 미묘한 관계를 연상케 합니다. 대개의 실용적인 함수 포인터 프로그래밍에서 신경쓰이는 부분은 아니긴 합니다만, 저처럼 pedantic한 프로그래밍 언어 문법 덕후들에게는 가끔 가다 떠올라서 신경쓰이게 만드는 부분이죠.

ymir의 이미지

function pointer 를 리턴하는 함수는 다음과 같이 쓰면 됩니다.
그런데 이렇게 쓰는 거는 가독성이 별로라서, 가급적 typedef 를 쓰는게 낫습니다.

#include <stdio.h>
 
void test2(void)
{
    printf("22\n");
}
 
void (*test1()) (void)
{
    return &test2;
}
 
int main(void)
{
    void (*g) (void);
 
    g = test1();
    (*g) ();
 
    return 0;
}

되면 한다! / feel no sorrow, feel no pain, feel no hurt, there's nothing gained.. only love will then remain.. 『 Mizz 』

익명 사용자의 이미지

여기까지는 생각을 못 했군요.
저는 void (*)(void) test(void) 정도로 기대하고 있었었는데 말이죠.
으 역시 배우고 또 배워도 끝이 없는 미묘한 C syntax...

일관성이 있다면 있다고 볼 수도 있겠습니다만 가독성은 정말 별로로군요. 레알 건축무한육면각체 수준...

ozon1000의 이미지

테스트 중인데 확실히 typedef를 쓰는데 가독성 및 코드 수정 등에 유리하더라고요.
생전 처음 보는 문법을 테스트 하는 중이라 그런지 어려움이 좀 있습니다.
코드를 한번 봐 주세요.
이게 너무 헷갈리는게 test1의 함수는 test2의 함수 포인터를 리턴해야 하는게 맞지 않나요?
main문 안의 g의 선언을 보시게 되면 나중에 함수 포인터를 받은 g는 두개의 int를 가져갈 수 있는데요.
이럴때 g의 선언에 뒤에 void 가 아닌 int, int라고 써도 괜찮은 건가요?

ymir의 코드와 제가 밑에 올려놓은 코드를 보고 코멘트를 달아주실 수 있으시나요?
읽어주셔서 감사합니다.

#include <stdio.h>
 
int ( *test1() )(void);
int test2(int a, int b);
 
int main()
{
	int(*g)(void);
	g = test1();
	printf("%d", g(1, 3));
}
 
int ( *test1() )(void)
{
	return test2;
}
 
int test2(int a, int b)
{
	return a + b;
}
<\code>
익명 사용자의 이미지

#include <stdio.h>
 
// int ( *test1() )(void);
int ( *test1(void) )(int, int); // 안쪽 (void)는 test1의 매개변수, 바깥쪽 (int, int)는 test1의 반환형의 매개변수
int test2(int a, int b);
 
// int main()
int main(void)
{
    // int(*g)(void);
    int(*g)(int, int);          // test1이 반환하는 함수는 (int, int)를 매개변수로 받고 int를 반환하는 함수이죠.
    g = test1();
    printf("%d", g(1, 3));
}
 
// int ( *test1() )(void)
int ( *test1(void) )(int, int)  // 위와 같음
{
    return test2;
}
 
int test2(int a, int b)
{
    return a + b;
}

C언어에서의 선언은 선언 대상을 감싸는 형태라고 보시는 편이 좋습니다.
즉, 선언 대상에 가까운 것부터 적용됩니다.

A (*i(B))(C)와 같은 선언이 있을 때, (B)가 먼저 i에 결합하여 i를 "함수"로 만듭니다. (후위에 있는 것이 먼저 결합)
매개변수로 (B)를 받는 함수요. 그럼 무엇을 리턴하느냐? *, 즉 포인터.
무엇에 대한 포인터냐면, (C)를 받는 함수.
그 함수의 반환값은 A 타입이 되는 셈이죠.

즉 정리하자면 i(B)를 매개변수로 받으며, (C)를 매개변수로 받아 A를 반환하는 함수에 대한 포인터를 반환하는 "함수"가 되는 겁니다.

C언어의 함수 선언 규칙은 참 복잡하고 재밌죠. 이런 퍼즐은 퍼즐로서 즐기고, 실용 코딩 때는 지양하는 편이 좋습니다.
한참 머리굴려서 코딩해 놓고 몇 주 뒤에 다시 보면 까먹어서 다시 풀어야 하거든요. -_-;

ozon1000의 이미지

한참 이것저것 만져보는 대학원생으로서,

심오한(?) 깊은 생각을 하게 해준 유익한 시간이었습니다.

친절하고 세밀한 답변 다시 한번 감사드립니다.

chanik의 이미지

아래와 같이 바꾸면 좀 생각하기 편하실까요?
test1()은 인덱스를 받아서 그에 해당하는 함수포인터를 골라 반환해주는 브로커(?) 역할을 합니다.
main 함수에서는 함수포인터인 g를 정의해 두고 브로커인 test1()를 통해 넘겨받은 함수포인터를 g에 할당한 다음, g에 저장한 함수를 실행합니다.

#include <stdio.h>
 
void *test1();
int test_add(int, int);
int test_sub(int, int);
int test_mul(int, int);
int test_div(int, int);
 
int main()
{
    int(*g)(int, int);                       // 함수포인터 g 정의. int 두 개를 받아 int를 반환하는 함수.
 
    g = test1(1); printf("%d\n", g(15, 7));  // 함수포인터 받아 g에 저장, 실행
    g = test1(2); printf("%d\n", g(15, 7));  //           "
    g = test1(3); printf("%d\n", g(15, 7));  //           "
    g = test1(4); printf("%d\n", g(15, 7));  //           "
 
    return 0;
}
 
void *test1(int idx)
{
    switch(idx)
    {
    case 1: return test_add;
    case 2: return test_sub;
    case 3: return test_mul;
    case 4: return test_div;
    }
    return NULL;
}
 
int test_add(int a1, int a2)
{
    return a1 + a2;
}
 
int test_sub(int a1, int a2)
{
    return a1 - a2;
}
 
int test_mul(int a1, int a2)
{
    return a1 * a2;
}
 
int test_div(int a1, int a2)
{
    return a1 / a2;
}
chanik의 이미지

위에 익명님 댓글에서 배울 점이 많네요..
그 댓글을 탐독하시면 되겠습니다

mauri의 이미지

KLDP를 끊지 못하는것 같아요.. ㅎㅎ
평소와 다르게 좀더 깊이 생각하게 하는 계기를 만들어주니 참 좋습니다~ !

ozon1000의 이미지

세상은 넓고 능력자는 많고!

멋지십니다. 답변 감사드립니다. 많은 도움이 되었습니다.

c / c++을 수박 겉핥기식으로 익히며 사용했다는 느낌이 팍팍(?) 들게하는 답변이었습니다.

댓글 달기

Filtered HTML

  • 텍스트에 BBCode 태그를 사용할 수 있습니다. URL은 자동으로 링크 됩니다.
  • 사용할 수 있는 HTML 태그: <p><div><span><br><a><em><strong><del><ins><b><i><u><s><pre><code><cite><blockquote><ul><ol><li><dl><dt><dd><table><tr><td><th><thead><tbody><h1><h2><h3><h4><h5><h6><img><embed><object><param><hr>
  • 다음 태그를 이용하여 소스 코드 구문 강조를 할 수 있습니다: <code>, <blockcode>, <apache>, <applescript>, <autoconf>, <awk>, <bash>, <c>, <cpp>, <css>, <diff>, <drupal5>, <drupal6>, <gdb>, <html>, <html5>, <java>, <javascript>, <ldif>, <lua>, <make>, <mysql>, <perl>, <perl6>, <php>, <pgsql>, <proftpd>, <python>, <reg>, <spec>, <ruby>. 지원하는 태그 형식: <foo>, [foo].
  • web 주소와/이메일 주소를 클릭할 수 있는 링크로 자동으로 바꿉니다.

BBCode

  • 텍스트에 BBCode 태그를 사용할 수 있습니다. URL은 자동으로 링크 됩니다.
  • 다음 태그를 이용하여 소스 코드 구문 강조를 할 수 있습니다: <code>, <blockcode>, <apache>, <applescript>, <autoconf>, <awk>, <bash>, <c>, <cpp>, <css>, <diff>, <drupal5>, <drupal6>, <gdb>, <html>, <html5>, <java>, <javascript>, <ldif>, <lua>, <make>, <mysql>, <perl>, <perl6>, <php>, <pgsql>, <proftpd>, <python>, <reg>, <spec>, <ruby>. 지원하는 태그 형식: <foo>, [foo].
  • 사용할 수 있는 HTML 태그: <p><div><span><br><a><em><strong><del><ins><b><i><u><s><pre><code><cite><blockquote><ul><ol><li><dl><dt><dd><table><tr><td><th><thead><tbody><h1><h2><h3><h4><h5><h6><img><embed><object><param>
  • web 주소와/이메일 주소를 클릭할 수 있는 링크로 자동으로 바꿉니다.

Textile

  • 다음 태그를 이용하여 소스 코드 구문 강조를 할 수 있습니다: <code>, <blockcode>, <apache>, <applescript>, <autoconf>, <awk>, <bash>, <c>, <cpp>, <css>, <diff>, <drupal5>, <drupal6>, <gdb>, <html>, <html5>, <java>, <javascript>, <ldif>, <lua>, <make>, <mysql>, <perl>, <perl6>, <php>, <pgsql>, <proftpd>, <python>, <reg>, <spec>, <ruby>. 지원하는 태그 형식: <foo>, [foo].
  • You can use Textile markup to format text.
  • 사용할 수 있는 HTML 태그: <p><div><span><br><a><em><strong><del><ins><b><i><u><s><pre><code><cite><blockquote><ul><ol><li><dl><dt><dd><table><tr><td><th><thead><tbody><h1><h2><h3><h4><h5><h6><img><embed><object><param><hr>

Markdown

  • 다음 태그를 이용하여 소스 코드 구문 강조를 할 수 있습니다: <code>, <blockcode>, <apache>, <applescript>, <autoconf>, <awk>, <bash>, <c>, <cpp>, <css>, <diff>, <drupal5>, <drupal6>, <gdb>, <html>, <html5>, <java>, <javascript>, <ldif>, <lua>, <make>, <mysql>, <perl>, <perl6>, <php>, <pgsql>, <proftpd>, <python>, <reg>, <spec>, <ruby>. 지원하는 태그 형식: <foo>, [foo].
  • Quick Tips:
    • Two or more spaces at a line's end = Line break
    • Double returns = Paragraph
    • *Single asterisks* or _single underscores_ = Emphasis
    • **Double** or __double__ = Strong
    • This is [a link](http://the.link.example.com "The optional title text")
    For complete details on the Markdown syntax, see the Markdown documentation and Markdown Extra documentation for tables, footnotes, and more.
  • web 주소와/이메일 주소를 클릭할 수 있는 링크로 자동으로 바꿉니다.
  • 사용할 수 있는 HTML 태그: <p><div><span><br><a><em><strong><del><ins><b><i><u><s><pre><code><cite><blockquote><ul><ol><li><dl><dt><dd><table><tr><td><th><thead><tbody><h1><h2><h3><h4><h5><h6><img><embed><object><param><hr>

Plain text

  • HTML 태그를 사용할 수 없습니다.
  • web 주소와/이메일 주소를 클릭할 수 있는 링크로 자동으로 바꿉니다.
  • 줄과 단락은 자동으로 분리됩니다.
댓글 첨부 파일
이 댓글에 이미지나 파일을 업로드 합니다.
파일 크기는 8 MB보다 작아야 합니다.
허용할 파일 형식: txt pdf doc xls gif jpg jpeg mp3 png rar zip.
CAPTCHA
이것은 자동으로 스팸을 올리는 것을 막기 위해서 제공됩니다.