首页 文章

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

提问于
浏览
0

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

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

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

我们假设对象已知"to the programmer"是成员函数指针指向的同一个类的实例 . 但是,呼叫站点上的类类型未知"to the compiler" . universal_mf_ptr 类型是"pointer to a member function of any class type"的占位符 . reinterpret_call 是告诉编译器"trust me, this call will be valid at runtime just push the address of object on the stack and emit an assembly instruction to call-indirect mf_ptr"的假设语法 . 它的命名类似于 reinterpret_cast ,它告诉编译器"trust me, this cast is valid at runtime, just do the cast."

事实证明,令人惊讶的是, universal_mf_ptr 是一个真实的东西,并且在标准中它并不是未定义的行为 . (根据下面的链接文章 . )成员函数指针可以reinterpret_cast到其他成员函数指针(甚至是不同/不兼容的类类型) . 然而,虽然它是标准的,但它不是便携式的(即并非所有编译器都实现了标准的这一部分) .

尝试实际使用(调用) reinterpret_cast 'ed成员函数指针时,未定义的行为发挥作用 . 根据标准,这是未定义的行为,但(根据链接文章)是在任何编译器上实现的,该编译器实现了向不相关类类型转换成员函数指针的(非可移植但标准)特性 . 作者的断言是,如果转换指针在标准中,那么应该调用转换指针 .

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

我怀疑是这个要求任意选择一个受害者类并断言一个成员函数指针是一个类,它实际上不是一个成员,是导致这个问题被投票的原因 . 许多这些不能/不应该是标准的论据以便以这种方式调用成员函数可以同样适用于演员,但后者已经在标准中 .

该技术是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,带有基类的仿函数等 . 所有这些替代方案都很简单的地方是它们都会导致编译器在对象中发出其他类和成员文件 . 您个人可能不会考虑这个问题,但是在存储大量成员函数指针的用例中,这会增加目标文件的大小和编译时间,而不仅仅是获取成员函数的地址 .

2 回答

  • 2

    不可以 . [expr.mptr.oper]中的措辞,自N4606起,如下:

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

    在示例 my_obj->*my_func 中, TTypeEraserUvoid ,它不满足条件,因此代码只是格式错误 . 我不知道有任何改变这个的建议 .


    对于代码的新版本,您现在使用 reinterpret_cast<TypeEraser*>(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) ) - char或unsigned char类型 .

    TypeEraser 不是 MyClass 的那些东西,所以它是未定义的行为 .

  • 3

    不,没有可移植的方法直接这样做 .

    但是在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> ,将构造函数视为非共享等)并且不能在目标文件中公开它们 .

相关问题