C ++에서 std :: function 또는 함수 포인터를 사용해야합니까?


142

C ++에서 콜백 함수를 구현할 때 여전히 C 스타일 함수 포인터를 사용해야합니까?

void (*callbackFunc)(int);

또는 std :: function을 사용해야합니까?

std::function< void(int) > callbackFunc;

9
콜백 함수가 컴파일 타임에 알려진 경우 대신 템플리트를 고려하십시오.
Baum mit Augen

4
콜백 함수를 구현할 때는 호출자가 필요로하는 모든 것을 수행해야합니다. 콜백 인터페이스 디자인 에 대한 질문이라면 여기에 충분한 정보가 없습니다. 콜백 수신자가 무엇을 하시겠습니까? 수신자에게 어떤 정보를 전달해야합니까? 통화 결과 수신자가 어떤 정보를 다시 전달해야합니까?
피트 베커

답변:


171

요컨대,std::function 이유가없는 한 사용하십시오.

함수 포인터는 일부 컨텍스트 를 캡처 할 수 없다는 단점이 있습니다. 예를 들어 람다 함수를 일부 컨텍스트 변수를 캡처하는 콜백으로 전달할 수는 없지만 캡처하지 않으면 작동합니다. 따라서 객체 (- this포인터)를 캡처해야 하므로 객체의 멤버 변수를 호출하는 것도 불가능합니다 . (1)

std::function(C ++ 11부터)는 주로 함수 를 저장 하는 것입니다 (전달하면 저장하지 않아도 됨). 따라서 콜백을 멤버 변수에 저장하려면 아마도 최선의 선택 일 것입니다. 그러나 저장하지 않으면 호출 할 때 (매우 작은) 오버 헤드가 발생한다는 단점이 있지만 (성능이 중요한 상황에서는 문제가 될 수 있지만 대부분의 경우) 좋은 "첫 번째 선택"입니다. 안됩니다). 매우 "유니버설"입니다. 일관성 있고 읽을 수있는 코드에 대해 많은 관심을 갖고 있고 모든 선택에 대해 생각하고 싶지 않은 경우 (즉, 단순하게 유지하려는 경우) std::function전달하는 모든 함수에 사용하십시오 .

세 번째 옵션에 대해 생각 : 당신이 다음 제공된 콜백 함수를 통해 뭔가를보고 작은 기능을 구현하는 데 약이 있다면하는 생각 템플릿 매개 변수 다음이 될 수 있는 호출 객체 , 즉, 함수 포인터, 펑터, 람다, a std::function, ... 여기의 단점은 (외부) 함수가 템플릿이되므로 헤더에 구현해야한다는 것입니다. 반면에 (외부) 함수의 클라이언트 코드가 콜백에 대한 호출을 "인식"하면 사용 가능한 정확한 유형 정보가 표시되므로 콜백에 대한 호출이 인라인 될 수 있다는 이점이 있습니다.

템플리트 매개 변수가있는 버전의 예 ( C ++ 11 이전 &대신 쓰기 &&) :

template <typename CallbackFunction>
void myFunction(..., CallbackFunction && callback) {
    ...
    callback(...);
    ...
}

다음 표에서 볼 수 있듯이 모든 장단점이 있습니다.

+-------------------+--------------+---------------+----------------+
|                   | function ptr | std::function | template param |
+===================+==============+===============+================+
| can capture       |    no(1)     |      yes      |       yes      |
| context variables |              |               |                |
+-------------------+--------------+---------------+----------------+
| no call overhead  |     yes      |       no      |       yes      |
| (see comments)    |              |               |                |
+-------------------+--------------+---------------+----------------+
| can be inlined    |      no      |       no      |       yes      |
| (see comments)    |              |               |                |
+-------------------+--------------+---------------+----------------+
| can be stored     |     yes      |      yes      |      no(2)     |
| in class member   |              |               |                |
+-------------------+--------------+---------------+----------------+
| can be implemented|     yes      |      yes      |       no       |
| outside of header |              |               |                |
+-------------------+--------------+---------------+----------------+
| supported without |     yes      |     no(3)     |       yes      |
| C++11 standard    |              |               |                |
+-------------------+--------------+---------------+----------------+
| nicely readable   |      no      |      yes      |      (yes)     |
| (my opinion)      | (ugly type)  |               |                |
+-------------------+--------------+---------------+----------------+

(1)이 한계를 극복하기위한 대안이 있습니다 (예 : 추가 데이터를 추가 매개 변수로 (외부) 함수에 전달 : myFunction(..., callback, data)will call) callback(data). 이것이 C 스타일의 "인수를 이용한 콜백"입니다. C ++에서 사용할 수 있지만 (WIN32 API에서 많이 사용되는 방식으로) C ++에서 더 나은 옵션이 있으므로 피해야합니다.

(2) 클래스 템플릿에 대해 이야기하지 않는 한, 즉 함수를 저장하는 클래스는 템플릿입니다. 그러나 이것은 클라이언트 측에서 함수의 유형이 콜백을 저장하는 객체의 유형을 결정한다는 것을 의미합니다. 실제 사용 사례에는 거의 옵션이 아닙니다.

(3) C ++ 11 이전 버전의 경우 boost::function


9
함수 포인터는 템플릿 매개 변수와 비교하여 호출 오버 헤드가 있습니다. 실행중인 코드는 값이 아닌 매개 변수의 유형으로 설명되므로 템플릿 매개 변수를 사용하면 최고 수준으로 내려가더라도 인라인을 쉽게 수행 할 수 있습니다. 그리고 템플릿 반환 유형에 저장되는 템플릿 함수 객체는 일반적이고 유용한 패턴입니다 (좋은 복사 생성자 std::function를 사용하면 즉시 외부에 저장해야하는 경우 지워진 템플릿 함수로 변환 할 수있는 효율적인 템플릿 함수를 생성 할 수 있습니다) 컨텍스트라고 함).
Yakk-Adam Nevraumont

1
@tohecz 이제 C ++ 11이 필요한지 언급하지 않습니다.
leemes

1
@Yakk Oh 물론 잊어 버렸습니다! 감사합니다.
leemes

1
@MooingDuck 물론 구현에 따라 다릅니다. 그러나 유형 기억이 작동하는 방식으로 인해 올바르게 기억한다면 간접적 인 방향이 하나 더 있습니까? 그러나 이제 다시 생각해 보니, 함수 포인터 나 캡처리스 람다를 할당하면 (일반적인 최적화)
leemes

1
@leemes : 오른쪽 함수 포인터 captureless 또는 람다를 들어, 그것은 한다고는 C 자 FUNC - PTR 같은 오버 헤드가있다. 여전히 파이프 라인 스톨 + 사소하게 인라인되지 않은 것입니다.
Mooing Duck

25

void (*callbackFunc)(int); C 스타일 콜백 함수 일 수 있지만 좋지 않은 디자인 중 끔찍하게 사용할 수없는 함수입니다.

잘 설계된 C 스타일 콜백 모습처럼 void (*callbackFunc)(void*, int);- 그것은이이 void*기능을 넘어 상태를 유지하기 위해 콜백을 수행하는 코드를 허용 할 수 있습니다. 이렇게하지 않으면 호출자가 전 세계적으로 상태를 저장하게됩니다. 이는 무례한 것입니다.

std::function< int(int) >int(*)(void*, int)대부분의 구현에서 호출 보다 약간 더 비쌉니다 . 그러나 일부 컴파일러는 인라인하기가 더 어렵습니다. std::function라이브러리로 들어갈 수있는 함수 포인터 호출 오버 헤드 ( '가장 빠른 대리자'등)에 필적 하는 복제 구현이 있습니다.

이제 콜백 시스템의 클라이언트는 콜백을 생성 및 제거 할 때 리소스를 설정하고 처리해야하며 콜백의 수명을 인식해야합니다. void(*callback)(void*, int)이것을 제공하지 않습니다.

때로는 코드 구조 (콜백의 수명이 제한되어 있음) 또는 다른 메커니즘 (등록 취소 콜백 등)을 통해 사용할 수 있습니다.

std::function 제한된 수명 관리 수단을 제공합니다 (물론 잊어 버렸을 때 객체의 마지막 사본이 사라짐).

일반적으로 std::function 성능 관련 문제 없는 한 하지 않을 입니다. 그렇다면 픽셀 당 콜백 대신 구조적 변경 사항을 살펴보고, 전달하는 람다를 기반으로 스캔 라인 프로세서를 생성하는 방법은 무엇입니까? 이는 함수 호출 오버 헤드를 사소한 수준으로 줄이는 데 충분합니다. ). 그런 다음 지속되면 delegate가능한 가장 빠른 대리인을 기반으로 작성 하고 성능 문제가 사라지는 지 확인합니다.

나는 대부분 레거시 API 또는 다른 컴파일러 생성 코드 간 통신을 위해 C 인터페이스를 만들기 위해 함수 포인터 만 사용합니다. 또한 점프 테이블, 유형 삭제 등을 구현할 때 내부 구현 세부 정보로 사용했습니다. 생성 및 소비하고 클라이언트 코드에서 사용할 수 있도록 외부에서 노출하지 않고 함수 포인터가 필요한 모든 것을 수행합니다. .

적절한 콜백 수명 관리 인프라가 있다고 가정하면 스타일 콜백 std::function<int(int)>으로 바뀌는 래퍼를 작성할 수 있습니다 int(void*,int). 따라서 모든 C 스타일 콜백 수명 관리 시스템에 대한 연기 테스트로서, 나는 포장이 std::function합리적으로 잘 작동 하는지 확인합니다 .


1
어디 void*에서 왔습니까? 왜 함수 이상의 상태를 유지하고 싶습니까? 함수는 필요한 모든 코드, 모든 기능을 포함해야하며 원하는 인수를 전달하고 무언가를 수정하고 반환하면됩니다. 외부 상태가 필요한 경우 functionPtr 또는 콜백이 왜 해당 수하물을 운반합니까? 콜백은 불필요하게 복잡하다고 생각합니다.
Nikos

@ nik-lz 의견에 C로 콜백의 사용 및 기록을 어떻게 가르쳐 줄지 잘 모르겠습니다. 또는 함수형 프로그래밍과 달리 절차 철학. 따라서 채워지지 않은 채로 둡니다.
Yakk-Adam Nevraumont

나는 잊었다 this. 멤버 함수가 호출되는 경우를 고려해야하기 때문에 this객체의 주소를 가리키는 포인터 가 필요 합니까? 내가 틀렸다면 그것에 대해 더 많이 찾을 수 없기 때문에 더 많은 정보를 찾을 수있는 링크를 줄 수 있습니까? 미리 감사드립니다.
Nikos

@ Nik-Lz 멤버 함수는 함수가 아닙니다. 함수는 (런타임) 상태가 아닙니다. 콜백 void*은 런타임 상태의 전송을 허용 하기 위해를 사용합니다. a void*void*인수가 있는 함수 포인터 는 객체에 대한 멤버 함수 호출을 에뮬레이션 할 수 있습니다. 죄송합니다. "C 콜백 메커니즘 설계 101"을 안내하는 리소스를 모릅니다.
Yakk-Adam Nevraumont

그래, 내가 말한거야 런타임 상태는 기본적으로 호출되는 객체의 주소입니다 (실행간에 변경되므로). 아직 약 this입니다. 그게 내 뜻이야 그래, 고마워
Nikos

17

사용하다 std::function임의의 호출 가능한 객체를 저장하는 데 합니다. 사용자는 콜백에 필요한 컨텍스트를 제공 할 수 있습니다. 일반 함수 포인터는 그렇지 않습니다.

어떤 이유로 든 C 함수 API를 원하기 때문에 일반 함수 포인터를 사용해야하는 경우 void * user_context인수에 추가해야 합니다 (최소한이지만 불편하지만). 함수.


여기서 p의 유형은 무엇입니까? std :: function 유형입니까? 무효 f () {}; 자동 p = f; 피();
sree

14

피해야 할 유일한 이유 std::function 는 C ++ 11에 도입 된이 템플릿에 대한 지원이없는 레거시 컴파일러의 지원입니다.

C ++ 11 이전 언어를 지원할 필요가없는 경우를 사용 std::function하면 호출자가 콜백을 구현할 때 더 많은 선택권 을 부여하므로 "일반"함수 포인터에 비해 더 나은 옵션이됩니다. 콜백을 수행하는 코드에 대한 구현의 세부 사항을 추상화하면서 API 사용자에게 더 많은 선택권을 제공합니다.


1

std::function 경우에 따라 VMT를 코드에 가져올 수 있습니다.


3
이 VMT가 어떤 모자인지 설명 할 수 있습니까?
Gupta
당사 사이트를 사용함과 동시에 당사의 쿠키 정책개인정보 보호정책을 읽고 이해하였음을 인정하는 것으로 간주합니다.
Licensed under cc by-sa 3.0 with attribution required.