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

Has or will C++14 or C++1z make it no longer undefined to call delegate class member function pointers?

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

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

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

我们假设已知对象 "to the programmer" 是成员函数指针所指向的同一个 class 的实例。但是 class 类型在调用站点是未知的 "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 指向其他成员函数指针(甚至 different/incompatible class 类型)。然而,尽管它是标准,但它不是可移植(即并非所有编译器都实现标准的这一部分)。

当尝试实际使用(调用)reinterpret_cast 编辑的成员函数指针时,未定义的行为开始发挥作用。根据 标准 ,这是未定义的行为,但是(根据链接文章)在任何实现将成员函数指针转换为(不可移植,但标准)功能的编译器上实现不相关的 class 类型。作者的断言是,如果强制转换指针符合标准,那么应该调用强制转换的指针。

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

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

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

Casting between member function pointers is an extremely murky area. During the standardization of C++, there was a lot of discussion about whether you should be able to cast a member function pointer from one class to a member function pointer of a base or derived class, and whether you could cast between unrelated classes. By the time the standards committee made up their mind, different compiler vendors had already made implementation decisions which had locked them into different answers to these questions. According to the Standard (section 5.2.10/9), you can use reinterpret_cast to store a member function for one class inside a member function pointer for an unrelated class. The result of invoking the casted member function is undefined. The only thing you can do with it is cast it back to the class that it came from. I'll discuss this at length later in the article, because it's an area where the Standard bears little resemblance to real compilers.

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

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、具有基数 class 的仿函数等。所有这些替代方法的不足之处在于它们都会导致编译器在目标文件中发出额外的 classes 和成员。您个人可能不认为这是一个问题,但在存储大量成员函数指针的用例中,这会增加目标文件的大小和编译时间,而不仅仅是获取成员函数的地址。

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

The binary operator ->* binds its second operand, which shall be of type “pointer to member of T” to its first operand, which shall be of type “pointer to U” where U is either T or a class of which T is an unambiguous and accessible base class.

例子my_obj->*my_func中,TTypeEraserUvoid,不满足条件,所以代码简单病态的。我不知道有任何更改此设置的建议。


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

If a program attempts to access the stored value of an object through a glvalue of other than one of the following types the behavior is undefined:
(8.1) — the dynamic type of the object,
(8.2) — a cv-qualified version of the dynamic type of the object,
(8.3) — a type similar (as defined in 4.5) to the dynamic type of the object,
(8.4) — a type that is the signed or unsigned type corresponding to the dynamic type of the object,
(8.5) — a type that is the signed or unsigned type corresponding to a cv-qualified version of the dynamic type of the object,
(8.6) — an aggregate or union type that includes one of the aforementioned types among its elements or nonstatic data members (including, recursively, an element or non-static data member of a subaggregate or contained union),
(8.7) — a type that is a (possibly cv-qualified) base class type of the dynamic type of the object,
(8.8) — a char or unsigned char type.

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

不,没有可移植的方法直接执行此操作。

但是在 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* 上工作。它假定类型匹配(完全匹配)。

我们现在要输入 erase this。

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