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 표준을 보았지만 ‘호환 함수 포인터’가 무엇을 의미하는지에 대한 정의를 찾을 수 없습니다.
답변
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에서 잘 작동하지만 정의되지 않은 동작임을 기억하십시오.
답변
최근 GLib의 일부 코드와 관련하여 정확히 동일한 문제에 대해 질문했습니다. (GLib은 GNOME 프로젝트의 핵심 라이브러리이며 C로 작성되었습니다.) 저는 전체 slots’n’signals 프레임 워크가 그것에 의존한다고 들었습니다.
코드 전체에서 유형 (1)에서 (2)로 캐스트하는 수많은 인스턴스가 있습니다.
typedef int (*CompareFunc) (const void *a,
const void *b)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)에서 컴파일 / 작업 / 테스트합니다. 표준은 저주받은 것 같아요.
나는 이러한 답변에 대해 생각하는 데 시간을 보냈습니다. 내 결론은 다음과 같습니다.
- 콜백 라이브러리를 작성하는 경우 괜찮을 수 있습니다. 주의 사항-자신의 책임하에 사용하십시오.
- 그렇지 않으면하지 마십시오.
이 응답을 작성한 후 더 깊이 생각하면 C 컴파일러 용 코드가 이와 동일한 트릭을 사용하더라도 놀라지 않을 것입니다. 그리고 (대부분 / 전체?) 현대 C 컴파일러는 부트 스트랩되기 때문에 트릭이 안전하다는 것을 의미합니다.
조사해야 할 더 중요한 질문 : 누군가이 트릭이 작동 하지 않는 플랫폼 / 컴파일러 / 링커 / 로더를 찾을 수 있습니까 ? 저것에 대한 주요 브라우니 포인트. 나는 그것을 좋아하지 않는 임베디드 프로세서 / 시스템이있을 것입니다. 그러나 데스크톱 컴퓨팅 (및 아마도 모바일 / 태블릿)의 경우이 트릭은 여전히 작동합니다.
답변
요점은 실제로 할 수 있는지 여부가 아닙니다. 사소한 해결책은
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에 대한 코드를 생성합니다.
답변
반환 유형과 매개 변수 유형이 호환되는 경우 호환되는 함수 유형이 있습니다. 기본적으로 (실제로는 더 복잡합니다 :)). 호환성은 “동일한 유형”과 동일하여 다른 유형을 가질 수 있도록 좀 더 느슨하지만 “이 유형은 거의 동일합니다”라고 말하는 형태가 있습니다. 예를 들어 C89에서 두 구조체는 동일하지만 이름 만 다른 경우 호환됩니다. C99가 그것을 바꾼 것 같습니다. 근거 문서 에서 인용 (강력히 권장되는 읽기, btw!) :
서로 다른 두 번역 단위의 구조, 공용체 또는 열거 형 선언은 번역 단위 자체가 분리되어 있기 때문에 이러한 선언의 텍스트가 동일한 포함 파일에서 온 경우에도 동일한 유형을 공식적으로 선언하지 않습니다. 따라서 표준은 이러한 유형에 대한 추가 호환성 규칙을 지정하므로 두 개의 선언이 충분히 유사하면 호환됩니다.
즉, 엄격하게 정의되지 않은 동작입니다. do_stuff 함수 또는 다른 사람이 void*
매개 변수 로있는 함수 포인터로 함수를 호출 하지만 함수에 호환되지 않는 매개 변수가 있기 때문입니다. 그러나 그럼에도 불구하고 나는 모든 컴파일러가 신음하지 않고 컴파일하고 실행하기를 기대합니다. 그러나 void*
실제 함수를 호출 하는 다른 함수를 가져 와서 콜백 함수로 등록 하면 더 깔끔하게 할 수 있습니다 .
답변
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
기본적으로 데이터가 런타임에 계속 의미가있는 한 원하는대로 포인터를 캐스팅 할 수 있습니다.
답변
함수 호출이 C / C ++에서 작동하는 방식을 생각하면 스택의 특정 항목을 푸시하고 새 코드 위치로 이동 한 다음 실행 한 다음 반환시 스택을 팝합니다. 함수 포인터가 동일한 반환 유형과 동일한 수 / 크기의 인수를 가진 함수를 설명하는 경우 괜찮습니다.
따라서 안전하게 할 수 있어야한다고 생각합니다.
답변
Void 포인터는 다른 유형의 포인터와 호환됩니다. 그것은 malloc과 mem 함수 ( memcpy
, memcmp
)가 작동 하는 방식의 중추입니다 . 일반적으로 C에서 (C ++ NULL
가 아닌) ((void *)0)
.
C99에서 6.3.2.3 (항목 1)을보십시오.
void에 대한 포인터는 불완전하거나 객체 유형에 대한 포인터로 또는 포인터에서 변환 될 수 있습니다.
