함수 포인터를 다른 유형으로 캐스팅


89

void (*)(void*)콜백으로 사용할 함수 포인터를 받는 함수 가 있다고 가정 해 보겠습니다 .

void do_stuff(void (*callback_fp)(void*), void* callback_arg);

이제 다음과 같은 기능이 있다면 :

void my_callback_function(struct my_struct* arg);

안전하게 할 수 있습니까?

do_stuff((void (*)(void*)) &my_callback_function, NULL);

나는 이 질문 을 보았고 '호환 함수 포인터'로 캐스트 할 수 있다고 말하는 일부 C 표준을 보았지만 '호환 함수 포인터'가 무엇을 의미하는지에 대한 정의를 찾을 수 없습니다.


1
나는 다소 초보자이지만 "void ( ) (void ) 함수 포인터"는 무엇을 의미합니까?. void *를 인수로 받아들이고 void를 반환하는 함수에 대한 포인터입니까?
Digital Gal

2
@Myke : .NET 과 같은 형식 서명이있는 함수에 대한 포인터를 void (*func)(void *)의미합니다 . 네, 당신 말이 맞습니다. funcvoid foo(void *arg)
mk12

답변:


122

C 표준에 관한 한, 함수 포인터를 다른 유형의 함수 포인터로 캐스팅 한 다음이를 호출하면 정의되지 않은 동작 입니다. 부록 J.2 (정보) 참조 :

다음 상황에서는 동작이 정의되지 않습니다.

  • 포인터는 유형이 가리키는 유형 (6.3.2.3)과 호환되지 않는 함수를 호출하는 데 사용됩니다.

섹션 6.3.2.3, 단락 8은 다음과 같습니다.

한 유형의 함수에 대한 포인터는 다른 유형의 함수에 대한 포인터로 변환되고 다시 역으로 변환 될 수 있습니다. 결과는 원래 포인터와 동일하게 비교됩니다. 변환 된 포인터를 사용하여 형식이 가리키는 형식과 호환되지 않는 함수를 호출하면 동작이 정의되지 않습니다.

즉, 함수 포인터를 다른 함수 포인터 유형으로 캐스트하고 다시 캐스트하고 호출하면 모든 것이 작동합니다.

호환성 의 정의 는 다소 복잡합니다. 섹션 6.7.5.3, 단락 15에서 찾을 수 있습니다.

두 함수 유형이 호환 되려면 둘 다 호환 가능한 반환 유형 127을 지정해야 합니다.

또한 매개 변수 유형 목록이 둘 다있는 경우 매개 변수 수와 생략 부호 종결자를 사용하는 데 동의해야합니다. 해당 매개 변수는 호환 가능한 유형을 가져야합니다. 한 유형에 매개 변수 유형 목록이 있고 다른 유형이 함수 정의의 일부가 아니고 빈 식별자 목록을 포함하는 함수 선언자에 의해 지정된 경우 매개 변수 목록에는 줄임표 종결자가 없어야하며 각 매개 변수의 유형은 다음과 같아야합니다. 기본 인수 승격을 적용한 결과 유형과 호환되어야합니다. 한 유형에 매개 변수 유형 목록이 있고 다른 유형이 식별자 목록 (비어있을 수 있음)을 포함하는 함수 정의에 의해 지정되는 경우, 둘 다 매개 변수 수에 동의해야합니다. 각 프로토 타입 매개 변수의 유형은 기본 인수 승격을 해당 식별자 유형에 적용한 결과 유형과 호환되어야합니다. (유형 호환성 및 복합 유형의 결정에서 함수 또는 배열 유형으로 선언 된 각 매개 변수는 조정 된 유형을 갖는 것으로 간주되고 규정 된 유형으로 선언 된 각 매개 변수는 선언 된 유형의 규정되지 않은 버전을 갖는 것으로 간주됩니다.

127) 두 함수 유형이``이전 스타일 ''인 경우 매개 변수 유형은 비교되지 않습니다.

두 유형이 호환되는지 여부를 결정하는 규칙은 섹션 6.2.7에 설명되어 있으며 길이가 길기 때문에 여기에서 인용하지 않겠지 만 C99 표준 (PDF) 초안 에서 읽을 수 있습니다 .

여기서 관련 규칙은 섹션 6.7.5.1, 단락 2에 있습니다.

두 가지 포인터 유형이 호환 되려면 둘 다 동일하게 규정되어야하고 둘 다 호환 가능한 유형에 대한 포인터 여야합니다.

A는 이후 따라서, void* 호환되지 않는 A를 struct my_struct*, 형의 함수 포인터 void (*)(void*)타입의 함수 포인터와 호환되지 않습니다 void (*)(struct my_struct*)함수 포인터의 캐스팅은 기술적으로 행동을 정의되지 않습니다 그래서.

그러나 실제로는 경우에 따라 함수 포인터를 캐스팅하여 안전하게 벗어날 수 있습니다. x86 호출 규칙에서 인수는 스택에 푸시되고 모든 포인터는 동일한 크기 (x86에서는 4 바이트, x86_64에서는 8 바이트)입니다. 함수 포인터를 호출하는 것은 스택의 인수를 푸시하고 함수 포인터 대상으로 간접 점프하는 것으로 요약되며 기계 코드 수준에서 유형에 대한 개념은 분명히 없습니다.

확실히 할 수없는 일 :

  • 다른 호출 규칙의 함수 포인터간에 캐스트. 스택을 엉망으로 만들고 최악의 경우 크래시가 발생하고 거대한 보안 허점으로 조용히 성공할 수 있습니다. Windows 프로그래밍에서는 종종 함수 포인터를 전달합니다. Win32에서 모든 콜백 함수가 사용할 것으로 예상 stdcall호출 규칙을 (이 매크로가 CALLBACK, PASCAL그리고 WINAPI모두에 확장). 표준 C 호출 규칙 ( cdecl) 을 사용하는 함수 포인터를 전달하면 불량이 발생합니다.
  • C ++에서 클래스 멤버 함수 포인터와 일반 함수 포인터간에 캐스트합니다. 이것은 종종 C ++ 초보자를 괴롭 힙니다. 클래스 멤버 함수에는 숨겨진 this매개 변수가 있으며 멤버 함수를 일반 함수로 캐스트하면 this사용할 개체가 없으며 다시 많은 나쁜 결과가 발생합니다.

때로는 작동하지만 정의되지 않은 동작이기도 한 또 다른 나쁜 아이디어 :

  • 함수 포인터와 일반 포인터 간의 캐스팅 (예 : a void (*)(void)를 a로 캐스팅 void*). 일부 아키텍처에서는 추가 컨텍스트 정보를 포함 할 수 있기 때문에 함수 포인터가 반드시 일반 포인터와 같은 크기는 아닙니다. 이것은 아마도 x86에서 잘 작동하지만 정의되지 않은 동작임을 기억하십시오.

18
요점은 void*다른 포인터와 호환된다는 것입니까? a struct my_struct*를으로 캐스팅하는 데 문제가 없어야합니다 void*. 실제로 캐스팅 할 필요도없고 컴파일러가 수락해야합니다. 예를 들어 struct my_struct*를받는 함수 에을 전달하면 void*캐스팅이 필요하지 않습니다. 이것들이 호환되지 않게 만드는 내가 여기서 무엇을 놓치고 있습니까?
brianmearns 2012 년

2
이 답변은 "이것은 아마도 x86에서 잘 작동 할 것입니다 ..."를 참조합니다 : 이것이 작동하지 않는 플랫폼이 있습니까? 이것이 실패했을 때 경험이있는 사람이 있습니까? C에 대한 qsort ()는 가능하다면 함수 포인터를 캐스팅하기에 좋은 곳처럼 보입니다.
kevinarpe 2012

4
@KCArpe : 이 기사의 "구성원 함수 포인터 구현"이라는 제목 아래의 차트에 따르면 16 비트 OpenWatcom 컴파일러는 특정 구성에서 데이터 포인터 유형 (2 바이트)보다 더 큰 함수 포인터 유형 (4 바이트)을 사용하는 경우가 있습니다. 그러나 POSIX 호환 시스템은 void*함수 포인터 유형과 동일한 표현을 사용해야합니다 . spec을 참조하십시오 .
Adam Rosenfield 2012

3
@adam의 링크는 이제 관련 섹션 2.12.3이 제거 된 POSIX 표준의 2016 에디션을 참조합니다. 2008 년판 에서 여전히 찾을 수 있습니다 .
마틴 Trenkmann

6
@brianmearns 아니요, 매우 정확하게 정의 된 방식으로void * 다른 (비 기능) 포인터 와 만 "호환"됩니다 (이 경우 C 표준이 "호환"이라는 단어로 의미하는 것과 관련이 없음). C는 a 가 a보다 크거나 작을 수 있도록 허용 하거나 비트를 다른 순서로 또는 부정하거나 무엇이든 가질 수 있습니다. 그래서 및 될 수 ABI - 호환되지 않는 . C는 필요한 경우 포인터 자체를 변환하지만 다른 인수 유형을 사용하도록 지시 함수를 변환하지 않거나 변환 할 수없는 경우도 있습니다. void *struct my_struct *void f(void *)void f(struct my_struct *)
mtraceur

32

최근 GLib의 일부 코드와 관련하여 정확히 동일한 문제에 대해 질문했습니다. (GLib은 GNOME 프로젝트의 핵심 라이브러리이며 C로 작성되었습니다.) 저는 전체 slots'n'signals 프레임 워크가 그것에 의존한다고 들었습니다.

코드 전체에서 유형 (1)에서 (2)로 캐스트하는 수많은 인스턴스가 있습니다.

  1. typedef int (*CompareFunc) (const void *a, const void *b)
  2. typedef int (*CompareDataFunc) (const void *b, const void *b, void *user_data)

다음과 같은 호출로 체인 스루가 일반적입니다.

int stuff_equal (GStuff      *a,
                 GStuff      *b,
                 CompareFunc  compare_func)
{
    return stuff_equal_with_data(a, b, (CompareDataFunc) compare_func, NULL);
}

int stuff_equal_with_data (GStuff          *a,
                           GStuff          *b,
                           CompareDataFunc  compare_func,
                           void            *user_data)
{
    int result;
    /* do some work here */
    result = compare_func (data1, data2, user_data);
    return result;
}

http://git.gnome.org/browse/glib/tree/glib/garray.c 에서 직접 확인하십시오 g_array_sort().

위의 답변은 상세하고 정확할 가능성이 높습니다 . 표준위원회에 참여하는 경우 . Adam과 Johannes는 잘 연구 된 답변에 대한 공로를 인정받을 만합니다. 그러나 실제로는이 코드가 잘 작동한다는 것을 알게 될 것입니다. 논란이 있습니까? 예. 다음을 고려하십시오. GLib는 다양한 컴파일러 / 링커 / 커널 로더 (GCC / CLang / MSVC)를 사용하여 다수의 플랫폼 (Linux / Solaris / Windows / OS X)에서 컴파일 / 작업 / 테스트합니다. 표준은 저주받은 것 같아요.

나는 이러한 답변에 대해 생각하는 데 시간을 보냈습니다. 내 결론은 다음과 같습니다.

  1. 콜백 라이브러리를 작성하는 경우 괜찮을 수 있습니다. 주의 사항-자신의 책임하에 사용하십시오.
  2. 그렇지 않으면하지 마십시오.

이 응답을 작성한 후 더 깊이 생각하면 C 컴파일러 용 코드가 이와 동일한 트릭을 사용하더라도 놀라지 않을 것입니다. 그리고 (대부분 / 전체?) 현대 C 컴파일러는 부트 스트랩되기 때문에 트릭이 안전하다는 것을 의미합니다.

조사해야 할 더 중요한 질문 : 누군가이 트릭이 작동 하지 않는 플랫폼 / 컴파일러 / 링커 / 로더를 찾을 수 있습니까 ? 저것에 대한 주요 브라우니 포인트. 나는 그것을 좋아하지 않는 임베디드 프로세서 / 시스템이있을 것입니다. 그러나 데스크톱 컴퓨팅 (및 아마도 모바일 / 태블릿)의 경우이 트릭은 여전히 ​​작동합니다.


10
확실히 작동하지 않는 곳은 Emscripten LLVM to Javascript 컴파일러입니다. 자세한 내용은 github.com/kripken/emscripten/wiki/Asm-pointer-casts 를 참조하십시오.
Ben Lings 2013 년

2
[정보 Upated 참조 Emscripten .
ysdx

4
@BenLings가 게시 한 링크는 조만간 중단됩니다. 그것은 공식적으로하는 이동 kripken.github.io/emscripten-site/docs/porting/guidelines/...
알렉스 잉킹에게

9

요점은 실제로 할 수 있는지 여부가 아닙니다. 사소한 해결책은

void my_callback_function(struct my_struct* arg);
void my_callback_helper(void* pv)
{
    my_callback_function((struct my_struct*)pv);
}
do_stuff(&my_callback_helper);

좋은 컴파일러는 정말 필요한 경우에만 my_callback_helper에 대한 코드를 생성합니다.


문제는 이것이 일반적인 해결책이 아니라는 것입니다. 기능에 대한 지식과 함께 사례별로 수행해야합니다. 이미 잘못된 유형의 기능이 있다면 멈춰 있습니다.
BeeOnRope

my_callback_helper항상 인라인되지 않는 한 이것을 테스트 한 모든 컴파일러는에 대한 코드를 생성합니다 . 이것이하는 경향이있는 유일한 것은이므로 반드시 필요하지 않습니다 jmp my_callback_function. 컴파일러는 아마도 함수의 주소가 다른지 확인하려고 할 수도 있지만, 불행히도 함수가 C99로 표시되어있는 경우에도이 inline작업을 수행합니다 (예 : "주소에 신경 쓰지 마십시오").
yyny

이것이 정확한지 잘 모르겠습니다. 위의 다른 답변 (@mtraceur에 의한)의 또 다른 댓글은 a가 a void *와 다른 크기 일 수 있다고 말합니다 struct *(그렇지 않으면 malloc깨질 수 있기 때문에 틀린 것 같지만 해당 댓글에는 5 개의 업 보트가 있으므로 약간의 크레딧을 제공합니다. @mtraceur가 맞다면 작성한 솔루션이 올바르지 않을 것입니다
cesss

@cesss : 크기가 다른 경우에는 전혀 문제가되지 않습니다. 전환은 void*여전히 작동해야합니다. 요컨대, void*더 많은 비트를 가질 수 있지만 a struct*void*해당 추가 비트로 캐스트하면 0이 될 수 있으며 캐스트 백은 해당 0을 다시 버릴 수 있습니다.
MSalters

@MSalters : void *이론 상으로는 struct *. 저는 C로 vtable을 구현하고 this있으며 가상 함수에 대한 첫 번째 인수로 C ++-ish 포인터를 사용하고 있습니다. 분명히 this"현재"(파생 된) 구조체에 대한 포인터 여야합니다. 따라서 가상 함수는 구현 된 구조에 따라 다른 프로토 타입이 필요합니다. void *this인수를 사용하면 모든 것이 수정 될 것이라고 생각 했지만 이제 정의되지 않은 동작이라는 것을 알게되었습니다 ...
cesss

6

반환 유형과 매개 변수 유형이 호환되는 경우 호환되는 함수 유형이 있습니다. 기본적으로 (실제로는 더 복잡합니다 :)). 호환성은 "동일한 유형"과 동일하여 다른 유형을 가질 수 있도록 좀 더 느슨하지만 "이 유형은 거의 동일합니다"라고 말하는 형태가 있습니다. 예를 들어 C89에서 두 구조체는 동일하지만 이름 만 다른 경우 호환됩니다. C99가 그것을 바꾼 것 같습니다. 근거 문서 에서 인용 (강력히 권장되는 읽기, btw!) :

서로 다른 두 번역 단위의 구조, 공용체 또는 열거 형 선언은 번역 단위 자체가 분리되어 있기 때문에 이러한 선언의 텍스트가 동일한 포함 파일에서 온 경우에도 동일한 유형을 공식적으로 선언하지 않습니다. 따라서 표준은 이러한 유형에 대한 추가 호환성 규칙을 지정하므로 두 개의 선언이 충분히 유사하면 호환됩니다.

즉, 엄격하게 정의되지 않은 동작입니다. do_stuff 함수 또는 다른 사람이 void*매개 변수 로있는 함수 포인터로 함수를 호출 하지만 함수에 호환되지 않는 매개 변수가 있기 때문입니다. 그러나 그럼에도 불구하고 나는 모든 컴파일러가 신음하지 않고 컴파일하고 실행하기를 기대합니다. 그러나 void*실제 함수를 호출 하는 다른 함수를 가져 와서 콜백 함수로 등록 하면 더 깔끔하게 할 수 있습니다 .


4

C 코드는 포인터 유형에 대해 전혀 신경 쓰지 않는 명령어로 컴파일되므로 언급 한 코드를 사용하는 것이 좋습니다. 콜백 함수와 다른 것에 대한 포인터로 do_stuff를 실행하고 my_struct 구조를 인수로 사용하면 문제가 발생합니다.

작동하지 않는 것을 보여줌으로써 더 명확하게 할 수 있기를 바랍니다.

int my_number = 14;
do_stuff((void (*)(void*)) &my_callback_function, &my_number);
// my_callback_function will try to access int as struct my_struct
// and go nuts

또는...

void another_callback_function(struct my_struct* arg, int arg2) { something }
do_stuff((void (*)(void*)) &another_callback_function, NULL);
// another_callback_function will look for non-existing second argument
// on the stack and go nuts

기본적으로 데이터가 런타임에 계속 의미가있는 한 원하는대로 포인터를 캐스팅 할 수 있습니다.


0

함수 호출이 C / C ++에서 작동하는 방식을 생각하면 스택의 특정 항목을 푸시하고 새 코드 위치로 이동 한 다음 실행 한 다음 반환시 스택을 팝합니다. 함수 포인터가 동일한 반환 유형과 동일한 수 / 크기의 인수를 가진 함수를 설명하는 경우 괜찮습니다.

따라서 안전하게 할 수 있어야한다고 생각합니다.


2
- struct포인터와- void포인터가 호환되는 비트 표현을 가지고있는 한 안전합니다 . 보장되지 않습니다
Christoph

1
컴파일러는 레지스터에 인수를 전달할 수도 있습니다. 그리고 부동 소수점, 정수 또는 포인터에 대해 다른 레지스터를 사용하는 것은 전례가 없습니다.
MSalters

0

Void 포인터는 다른 유형의 포인터와 호환됩니다. 그것은 malloc과 mem 함수 ( memcpy, memcmp)가 작동 하는 방식의 중추입니다 . 일반적으로 C에서 (C ++ NULL가 아닌) ((void *)0).

C99에서 6.3.2.3 (항목 1)을보십시오.

void에 대한 포인터는 불완전하거나 객체 유형에 대한 포인터로 또는 포인터에서 변환 될 수 있습니다.


이것은 Adam Rosenfield의 답변 과 모순됩니다 . 마지막 단락 및 주석 참조
user

1
이 대답은 분명히 틀 렸습니다. 함수 포인터를 제외한 모든 포인터는 void 포인터로 변환 할 수 있습니다.
marton78
당사 사이트를 사용함과 동시에 당사의 쿠키 정책개인정보 보호정책을 읽고 이해하였음을 인정하는 것으로 간주합니다.
Licensed under cc by-sa 3.0 with attribution required.