C++14 或 C++1z 是不是已经或将不再定义调用委托类成员函数指针?

Posted

技术标签:

【中文标题】C++14 或 C++1z 是不是已经或将不再定义调用委托类成员函数指针?【英文标题】:Has or will C++14 or C++1z make it no longer undefined to call delegate class member function pointers?C++14 或 C++1z 是否已经或将不再定义调用委托类成员函数指针? 【发布时间】:2016-09-13 14:47:26 【问题描述】:

由于这个问题似乎引起了一些争论,我正在编辑它,首先用假设的语法显示意图,然后显示一个实现。该实现依赖于令人惊讶的类型转换,然后调用这种类型转换的指针。问题是类型转换是标准的(尽管不可移植的)C++,但调用它的结果是未定义的行为。我的问题是标准是否最近或可能很快将调用类型转换的成员函数指针的结果更改为不再是未定义的行为。

目的是能够编写如下代码:

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 成员函数指针时,未定义的行为开始发挥作用。根据标准,这是未定义的行为,但是(根据链接的文章)在任何实现将成员函数指针转换为不相关类类型的(不可移植但标准)特性的编译器上实现.作者的断言是,如果转换指针在标准中,那么应该调用转换后的指针。

在任何情况下,如果希望利用将成员函数指针转换为通用成员函数指针类型的(标准的,不是未定义的,但不可移植的)特性,例如将异构成员函数存储在一个集合中,有必要任意指定一个“受害者”类作为类型转换的目标。这个类不需要像它被断言的那样有任何成员函数,实际上它可能没有成员,或者只是前向声明并且未定义。

我怀疑这是任意选择受害者类并断言成员函数指针属于一个实际上不是其成员的类的要求是导致该问题被否决的原因。许多关于 this 不能/不应该是标准的论点,以便以这种方式调用成员函数可以同样适用于 cast,但后者已经在标准。

技术是described in this article,但它会发出警告:

成员函数指针之间的转换是一个非常模糊的区域。在 C++ 的标准化过程中,有很多关于是否应该能够将成员函数指针从一个类强制转换为基类或派生类的成员函数指针,以及是否可以在不相关的类之间强制转换的讨论。当标准委员会做出决定时,不同的编译器供应商已经做出了实施决策,这些决策将他们锁定在对这些问题的不同答案中。根据标准(第 5.2.10/9 节),您可以使用 reinterpret_cast 将一个类的成员函数存储在不相关类的成员函数指针中。调用转换成员函数的结果是未定义的。你唯一能用它做的就是把它扔回它来自的类。我将在本文后面详细讨论这一点,因为这是标准与真实编译器几乎没有相似之处的领域。

您为什么要这样做?这样您就可以在同一个容器中存储指向许多不同对象类的成员函数指针,并在运行时选择一个来调用。 (假设代码还在运行时跟踪哪些成员函数指针可以合法调用哪些对象。)

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();

根据上面链接的文章,在实际实现转换和存储成员函数指针的编译器上,调用也可以按预期工作。但是(同样,根据文章)并非所有编译器都真正实现了转换和存储。也就是说,转换和存储是标准的,但它是不可移植的,而调用成员函数指针不是标准的,但如果前者有效,则它可以工作。如果两者都是标准的和便携的,那就更好了。

是的,有几种替代方法可以实现相同的目标:lambdas、带有基类的函子等。所有这些替代方法的不足之处在于它们都会导致编译器发出额外的类和成员在目标文件中。您个人可能不认为这是一个问题,但是在存储大量成员函数指针的用例中,这会增加目标文件的大小和编译时间,而不仅仅是获取成员函数的地址。

【问题讨论】:

我看不出为什么这个不应该是未定义的行为。从根本上说,这与将void(*)(MyClass*) 函数指针转换为void(*)(void*) 指针然后使用MyClass* 调用后者没有什么不同。那也是UB。在大多数编译器上您可能可以摆脱它的事实是无关紧要的。从语义上讲,这是废话 “在大多数编译器上,这确实有效。” ——不,它没有。很可能有一些微不足道的情况,它可以满足您的期望,但是使用指向其他类的对象的 void* 调用随机类的成员函数不需要工作,事实上,不会一般工作。特别是,当您为最重要的客户演示程序时,它总是会失败。 在否决我之前,请阅读链接的文章。那篇文章的作者进行了实证调查,并提出了一个合乎逻辑的论点,为什么从标准的其他部分可以得出调用成员函数的能力必须是定义的行为。 他给出了一个在前向声明的类上调用成员函数指针的代码示例,并说:“请注意,编译器必须生成汇编代码来调用成员函数指针,而对 SomeClass 类一无所知. 显然,除非链接器进行一些非常复杂的优化,否则无论类的实际定义如何,代码都必须正常工作。直接的结果是您可以安全地调用从完全不同的课程。” @Dennis:“提出一个合乎逻辑的论点,为什么它遵循标准的其他部分,调用成员函数的能力必须定义为行为。”我可以提出一个论点,解释为什么我给出的函数指针示例“必须定义行为”(或者使用更准确的术语,“必须工作”)。但是标准明确地使它未定义。因此,它不是定义的行为。这里也一样。 【参考方案1】:

没有。从 N4606 开始,[expr.mptr.oper] 中的措辞如下:

二元运算符-&gt;* 将其第二个操作数绑定到其第一个操作数,该操作数应为“指向T 成员的指针”类型 操作数,其类型应为“指向U”的指针,其中UTT 是明确的类 和可访问的基类。

在例子my_obj-&gt;*my_func中,TTypeEraserUvoid,不满足条件,所以代码就是病态的。我不知道有任何改变这一点的提议。


对于代码的新版本,您现在使用 reinterpret_cast&lt;TypeEraser*&gt;(obj) 代替,因此类型匹配...仍然没有,根据 [basic.lval]:

如果一个程序试图通过 Glvalue 访问一个对象的存储值,而不是其中一个 以下类型的行为未定义: (8.1)——对象的动态类型, (8.2) — 对象的动态类型的 cv 限定版本, (8.3) — 与对象的动态类型类似(如 4.5 中定义)的类型, (8.4) — 一种类型,即对应于对象的动态类型的有符号或无符号类型, (8.5) — 有符号或无符号类型,对应于动态类型的 cv 限定版本 对象, (8.6) — 在其元素中包含上述类型之一的聚合或联合类型或非静态类型 数据成员(递归地包括子聚合的元素或非静态数据成员或 包含联合), (8.7) — 对象动态类型的(可能是 cv 限定的)基类类型, (8.8) — charunsigned char 类型。

TypeEraserMyClass 无关,因此它是未定义的行为。

【讨论】:

根据我给出的示例代码的第一个版本,您的回答在技术上是正确的,但与问题的意图背道而驰。我在示例代码中有一个错误。我的意思是说 my_obj 是从 MyClass* 转换为 void* 到 TypeEraser* ,然后调用从 MyClass::* 转换为 TypeEraser::* 的成员函数指针。 @Dennis 不是真的,新版本的代码只是基于不同的部分是错误的。 更不用说 [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&lt;&amp;MyClass::MyFunc&gt; 可以在void*s 上工作。它假定类型匹配(完全匹配)。

我们现在要键入擦除这个。

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&lt;auto&gt;,将构造函数视为非共享等)并且不在您的目标文件中公开它们是可能的。

【讨论】:

感谢您的回答!一些 cmets - 您实际上不需要模板 用于 magic_mem_func 中的 ptr,因为函数的地址是编译时间常数;可以像声明 int 模板参数一样将函数指针声明为模板参数。但是我怀疑模板 将允许结构模板以以前我们必须使用函数模板的间接层来实现的方式进行类型推导,而且我不知道新的模板 功能,所以我在这里学到了一些东西! @Dennis 是的,你必须做一些丑陋的事情,比如bar&lt;decltype(&amp;a::foo), &amp;a::foo&gt;,这太恶心了!我想这足以测试它。 第二条评论是我在这个示例代码中看到了很多 lambda。这段代码是否实现了仅通过将一个函数调用包装在一个 lambda 中无法完成的事情?我不确定代码用额外的 lambda 演示了什么。谢谢! 其实我依稀记得曾经写过类似bar的宏! @Dennis (object-&gt;*func)(args...)-&gt;* 运算符的传统工作方式。为了复制它,我让我的魔术成员函数指针返回一个 lambda,它存储对象和操作(通过引用),然后接受 (args...)magic_mem_fununiversal_mem_fun_ptr 都支持 -&gt;* 这种方式;您可以通过修改universal 中的类型擦除构造函数来直接进行强制转换并使用ptr,轻松地将其减少到两个级别的lambda。我遵循 DRY,并重用 magic_mem_fun 的实现。 ctor 中的 lambda 是必需的,因为它是 ufmp 的 -&gt;*

以上是关于C++14 或 C++1z 是不是已经或将不再定义调用委托类成员函数指针?的主要内容,如果未能解决你的问题,请参考以下文章

苹果iPhone14系列或不再有iPhone mini?

在 SQL 中使用多个连接时,将所有内容连接到表 A 或将表 A 连接到表 B、表 B 到表 C 等是不是更快? [复制]

OCP考试专项 [1z0-071]-Q4:TO_CHAR/TO_DATE(2020.06.14)

c++1z动态异常规范错误

通过调整控制台大小或将其移出屏幕时,通过SetPixel设置的C ++像素正在消失

Oracle 12C--1z0-062