2016-09-13 3 views
0

이 질문은 약간의 논쟁의 원인으로 보이므로, 나는 가상의 구문으로 의도를 보여주고 구현을 보여주기 위해 편집 중입니다. 구현은 놀랍도록 타입 캐스트와이 타입 캐스트 된 포인터의 호출에 의존합니다. 문제는 형식 캐스트가 표준 (C++이긴하지만)이지만 결과를 호출하는 것은 정의되지 않은 동작입니다. 내 질문에 대한 표준 최근에 있거나 형식 캐스트 멤버 함수 포인터를 더 이상 정의되지 않은 동작을 호출 한 결과를 곧 변경할지 여부를 우려합니다. 멤버 함수 포인터가 가리키는대로 우리는 객체가 같은 클래스의 인스턴스가 될 "프로그래머"이라고 가정C++ 14 또는 C++ 1z에서 대리자 클래스 멤버 함수 포인터를 호출하기 위해 더 이상 정의되지 않았습니까?

void* object = ...; universal_mf_ptr mf_ptr = ...; 
reinterpret_call(object, mf_ptr); 

:

목적은 같은 코드를 쓸 수있다 . 그러나 클래스 유형은 호출 사이트에서 "컴파일러에"알려지지 않습니다. 형식 universal_mf_ptr은 "모든 클래스 형식의 멤버 함수에 대한 포인터"의 자리 표시 자입니다. reinterpret_call은 컴파일러에게 "이 호출은 런타임에 유효 할 것입니다. 스택에서 객체의 주소를 누르고 호출하여 간접 mf_ptr을 호출하는 어셈블리 명령어를 내 보냅니다."라는 사실을 가정하는 구문입니다. 이것은 컴파일러에게 "신뢰해라,이 캐스트는 런타임에 유효하다. 그냥 캐스트를한다."라고 비유하여 reinterpret_cast과 비슷한 이름을 붙였다.

놀랍게도 universal_mf_ptr은 진짜이며 표준에서는 정의되지 않은 동작이 아닙니다. (아래의 링크 된 기사에 따르면) 멤버 함수 포인터는 다른 멤버 함수 포인터 (다른/호환되지 않는 클래스 유형의 경우에도)에 reinterpret_cast 될 수 있습니다. 그러나 표준 인이지만 휴대용이 아닙니다 (즉 모든 컴파일러가 표준의이 부분을 구현하지는 않습니다).

정의되지 않은 동작은 실제로 reinterpret_cast의 ed 멤버 함수 포인터를 사용 (호출)하려고 할 때 사용됩니다. 이것은 표준 인에 따라 정의되지 않은 동작이지만 링크 된 아티클에 따라 관련없는 클래스 유형에 대한 멤버 함수 포인터를 캐스팅하는 (이식 가능하지 않지만 표준이지만) 기능을 구현하는 모든 컴파일러에서 구현됩니다. 작성자의 주장은 포인터를 표준으로 캐스팅하면 캐스팅 된 포인터를 호출해야한다는 것입니다.

임의의 경우, 하나의 컬렉션에 이기종 멤버 함수를 저장하는 것과 같이 범용 멤버 함수 포인터 유형에 대한 캐스팅 멤버 함수 포인터의 (표준, 미정도는 아니지만 이식 가능하지 않은) 기능을 활용해야 할 경우 , "희생자"클래스를 타입 캐스트의 대상으로 임의로 지정해야합니다. 이 클래스는 멤버 함수를 가질 필요가 없습니다. 실제로 멤버가 없거나 선언 된 채로 선언되지 않고 정의되지 않은 상태 일 수 있습니다.

나는 victim 클래스를 임의로 선택하고 멤버 함수 포인터가 실제로 멤버가 아닌 클래스라고 주장하는 것이이 질문의 원인이 무엇인지 의심 스럽습니다. 많은 사람들이 이런 식으로 표준이 될 수는 없으며 표준이되어서는 안되기 때문에 멤버 함수를 사용하면 캐스트도 똑같이 적용될 수 있습니다. 그러나 후자는 이미 표준에 있습니다.

기술은 described in this article이지만 경고 :

멤버 함수 포인터간에 매우 어두운 영역을 주조한다.C++의 표준화 중에 한 클래스의 멤버 함수 포인터를 기본 클래스 나 파생 클래스의 멤버 함수 포인터로 캐스팅 할 수 있어야하는지, 관련없는 클래스간에 캐스트 할 수 있는지 여부에 대한 논의가 많이있었습니다. 표준위원회가 마음을 굳히기 전까지는 여러 컴파일러 공급 업체가 이미 구현 결정을 내 렸으며 이러한 결정을 통해 이러한 질문에 대한 다른 대답을 얻을 수있었습니다. 표준 (5.2.10/9 절)에 따르면 reinterpret_cast를 사용하여 관련없는 클래스의 멤버 함수 포인터 안에 한 클래스의 멤버 함수를 저장할 수 있습니다. casted 멤버 함수를 호출 한 결과는 정의되지 않습니다. 여러분이 할 수있는 유일한 일은 그것이 나온 클래스로 다시 캐스팅하는 것입니다. 표준에서 실제 컴파일러와 거의 유사하지 않은 영역이기 때문에이 기사 뒷부분에서이 문제에 대해 자세히 설명하겠습니다.

왜이 작업을 원하십니까? 따라서 동일한 컨테이너에 여러 클래스의 객체에 대한 멤버 함수 포인터를 저장하고 런타임에 호출 할 함수 포인터를 선택할 수 있습니다. (코드는 멤버 함수 포인터가 객체되는 호출 합법적 런타임 추적 것으로 가정한다.) 주조 멤버 함수 포인터를 저장하는 것은 실제로 구현되는 컴파일러에 상기 링크 된 문서 당

class TypeEraser; // Not a base of anything. 
typedef void (TypeEraser::*erased_fptr)(); 
map<string, erased_fptr> functions; 

// Casting & storage as if member function of unrelated class is in the standard 
functions["MyFunc"] = reinterpret_cast<erased_fptr>(&MyClass::MyFunc); 

TypeEraser* my_obj = (TypeEraser*)(void*)new MyClass; 
erased_fpr my_func = functions["MyFunc"]; 

// !!! But calling it is undefined behavior according to standard !!! 
my_obj->*my_func(); 

, 호출도 예상대로 작동합니다. 그러나 (다시 말하지만) 모든 컴파일러가 실제로 캐스팅과 저장을 구현하는 것은 아닙니다. 즉, 캐스팅 및 저장은 표준이지만 이식 가능하지는 않지만 멤버 함수 포인터를 호출하는 것은 표준이 아니지만 이전 함수가 작동하는 경우 작동합니다. 둘 다 표준적이고 이식성이 있다면 더 좋을 것입니다.

그렇습니다. 람다, 기본 클래스를 가진 펑터 등등이 목표를 달성하기위한 몇 가지 다른 방법이 있습니다. 이러한 모든 대안이 짧아지는 곳은 모두 컴파일러가 추가 클래스를 방출하도록합니다. 개체 파일의 멤버. 개인적으로 문제를 고려하지 않을 수도 있지만 많은 수의 멤버 함수 포인터가 저장되는 유스 케이스에서는 단순히 멤버 함수의 주소를 취하는 것보다 훨씬 많은 객체 파일과 컴파일 시간을 필요로합니다.

+0

왜 * * 정의되지 않은 동작이되어서는 안되는 이유는 없습니다. 기본적으로'void (*) (void *)'포인터에'void (*) (MyClass *)'함수 포인터를 캐스팅 한 후'MyClass * '로 후자를 호출하는 것과 다르지 않습니다. 저도 UB입니다. 대부분의 컴파일러에서이 문제를 해결할 수 있다는 사실은 부적절합니다. 의미 론적으로, 그것은 말도 안돼. –

+0

"대부분의 컴파일러에서, 이것은 어쨌든 작동합니다." 아니, 그렇지 않아. 당신이 기대하는 일을하는 사소한 경우가있을 수 있지만, 다른 클래스의 객체를 가리키는'void * '를 가진 임의의 클래스의 멤버 함수를 호출하는 것은 작동하지 않아도되고 실제로, 일반적으로 작동합니다.특히, 가장 중요한 고객을 위해 프로그램을 시연 할 때 항상 실패합니다. –

+0

나를 downvoting하기 전에 링크 된 기사를 읽어보십시오. 이 기사의 저자는 실증적 인 조사와 표준의 다른 부분에서 멤버 함수를 호출하는 기능이 정의 된 동작이어야한다는 논리적 인 주장을 제시했습니다. – Dennis

답변

3

번호 [expr.mptr.oper]에서의 표현은, N4606로, 읽

이진 연산자 ->* 유형의 T의 부재 "포인터한다 번째 피연산자를 결합

"첫 번째 피연산자로, U에 대한 포인터는 이거나 T 인 클래스가 명확하고 이고 액세스 가능한 기본 클래스 인 형식이어야합니다. 예 my_obj->*my_func에서

, TTypeEraser이고 U이 조건을 만족하지 않는 void,이기 때문에 코드는 단순히 잘못 형성된다. 나는 이것을 바꿀 제안을 모른다. 유형이 일치 그래서 지금 대신 reinterpret_cast<TypeEraser*>(obj)를 사용하는 코드의 새로운적인 버전에 대한


... 여전히, [basic.lval]에 따라 :

프로그램을 시도하는 경우 다음 유형 중 하나 이외의 glvalue를 통해 객체의 저장된 값에 액세스합니다. 동작이 정의되지 않음 :
(8.1) - 객체의 동적 유형
(8.2) - 동적 유형의 개체,
(8.3) - 객체의 동적 유형에 해당하는 유형 (4.5에서 정의)
(8.4) - 객체의 동적 유형에 해당하는 부호가 있거나 부호가없는 유형 인 유형
(8.5) -
(8.6) - 해당 요소 중 위에서 언급 한 유형 중 하나 또는 비 정적 인 데이터를 포함하는 집계 또는 공용체 유형 (해당 유형의 동적 유형)에 해당하는 부호가 있거나 서명되지 않은 유형입니다. 멤버 (재귀 적으로 하위 집합의 요소 또는 비 정적 데이터 멤버 또는 합집합을 포함하는 ),
(8.7) - 개체의 동적 형식의 기본 클래스 형식 (가능하면 cv 한정) 인 형식 ,
(8.8) - char 또는 unsigned char 유형.

TypeEraserMyClass에 대한 것들 하나도 없다, 그래서 정의되지 않은 동작입니다.

+0

귀하의 답변은 제가 제공 한 예제 코드의 첫 번째 버전을 기준으로 기술적으로 정확하지만 질문의 의도에 어긋납니다. 나는 예제 코드에서 실수를했다. my_obj가 MyClass *에서 TypeEraser *로 void *로 캐스팅되었다고 말하면서 MyClass :: *에서 TypeEraser :: *로 캐스트 된 멤버 함수 포인터가 호출됩니다. – Dennis

+0

@Dennis 사실, 코드의 새 버전은 다른 섹션을 기반으로 잘못되었습니다. – Barry

+1

[expr.reinterpret.cast]/10은 물론 멤버 포인터를 통해 잘못된 유형으로 호출하려고 할 때 발생하는 일을 말하지 않습니다. 그것은 라운드 트립이 합법적 일 것이라고 만 말합니다. –

2

아니요, 직접이 작업을 수행 할 수있는 휴대용 방법이 없습니다.

그러나 C++ 17에서는 친숙해질 수 있습니다.

template<auto ptr> 
struct magic_mem_fun; 

template<class T, class R, class...Args, R(T::*ptr)(Args...)> 
struct magic_mem_fun<ptr> { 
    friend R operator->*(void* lhs, universal_mem_fun) { 
    return [lhs = (T*)lhs](Args...args)->R { 
     return (lhs->*ptr)(std::forward<Args>(args)...); 
    }; 
    } 
}; 

지금 magic_mem_fun_ptr<&MyClass::MyFunc>void*의 작업을 할 수 있습니다. 유형 일치 (정확히)를 가정합니다.

이제 이것을 지워야합니다.

template<class Sig> 
struct universal_mem_fun_ptr; 

template<class R, class...Args> 
struct universal_mem_fun_ptr<R(Args...)> { 
    R(*f)(void*, Args...) = nullptr; 
    template<class T, class R, class...Args, R(T::*ptr)(Args...)> 
    universal_mem_fun_ptr(magic_mem_ptr<ptr>): 
    f([](void* t, Args... args)->R { 
     return (t->*magic_mem_ptr<ptr>{})(std::forward<Args>(args)...); 
    }) 
    {} 
    friend R operator->*(void* t, universal_mem_fun_ptr f) { 
    return [=](Args...args)->R{ 
     return f.f(t, std::forward<Args>(args)...); 
    }; 
    } 
}; 

와 나는 우리가 얻을 생각 완벽하게 합법적

universal_mem_fun_ptr<void()> MyFunc = magic_mem_fun<&MyClass::MyFunc>{}; 

auto my_class = std::make_unique<MyClass>(); 

void* type_erased = (void*)my_class.get(); 

(type_erased->*MyFunc)(); 

내가 auto 템플릿 인수 컴파일러를 가지고 있지 않는 한 나는,이 테스트 할 수 없습니다, 나는 바로 그것을 가지고 있다면이 불확실입니다.

이 함수는 모든 것을 단일 함수 포인터에 저장합니다. 멤버 함수 포인터의 런타임 유형 삭제 (멤버 함수 포인터에 대한 컴파일 시간 지식을 가지고있는 지점에서 지우기와 반대)를 원할 경우 universal_mem_fun_ptr은 단일 함수 포인터보다 많은 상태를 저장해야합니다.

universal_mem_fun_ptr에서 Sig을 추론하는 것은 행할 수 있어야한다,하지만 난 운동으로 떠날 것입니다.

인수가 여러 번 전달되므로 값 비싼 경우 성능이 저하 될 수 있습니다. 전달 참조를 매우주의 깊게 사용하면 중간 이동 중 일부는 피할 수 있지만 모든 중하 중을 피할 수는 있습니다.

이러한 유형의 대부분을 삭제하도록 컴파일러에 알리고 (magic_mem_fun_ptr<auto>을 생성하지 않고 생성자를 비공유로 처리하는 등) 개체 파일에 노출시키지 않을 수도 있습니다.

+0

답변 해 주셔서 감사합니다. 몇가지 코멘트 - 함수의 주소가 컴파일 타임 상수이기 때문에 실제로 magic_mem_func에서 ptr을 위해 템플릿 을 필요로하지 않습니다. int 템플릿 매개 변수를 선언하는 것과 같은 방법으로 함수 포인터를 템플릿 매개 변수로 선언 할 수 있습니다. 그러나 템플릿 은 구조 템플릿이 이전에 함수 템플릿을 사용하여 간접적 인 계층을 구현해야했던 방식으로 형식 차감을 할 수 있다고 생각합니다. 새 템플릿 을 인식하지 못했기 때문에 여기에서 뭔가를 배웠습니다. ! – Dennis

+0

@Dennis 네,'bar '와 같은 추악한 짓을해야합니다. 나는 그것을 테스트하는 것으로 충분할 것이라고 생각합니다. – Yakk

+0

두 번째 주석은이 예제 코드에서 많은 lambdas를 보았다는 것입니다. 이 코드는 하나의 함수 호출을 하나의 람다로 묶는 것만으로는 수행 할 수없는 것을 달성합니까? 코드가 추가 lambdas로 시연하고 있는지 확실하지 않습니다. 감사! – Dennis