Skip to content

Instantly share code, notes, and snippets.

@rjmccall
Last active March 30, 2020 22:10
Show Gist options
  • Save rjmccall/9fc2db2ab41303d3332b71c4c2c69808 to your computer and use it in GitHub Desktop.
Save rjmccall/9fc2db2ab41303d3332b71c4c2c69808 to your computer and use it in GitHub Desktop.
[expr.unary.op] permits forming a member pointer that selects a virtual function. A call to such a member pointer performs a virtual function call ([expr.call]p3), and equality comparisons involving such a member pointer have unspecified results except that they compare unequal to the null member pointer ([expr.eq]p4).
The predominant implementation strategy for non-virtual member function pointers is to simply store the address of the member function. There are two implementation strategies for virtual member function pointers:
1. Store the address (as if forming a non-virtual member pointer) of an implicit, inline, non-virtual member function (a "thunk") which performs the requested virtual call.
2. Store the offset of the target virtual function within the class's virtual function table, in some way that can be reliably distinguished from a pointer to a non-virtual member function.
Both of these strategies are in use today. Unfortunately, it is unclear whether the use of thunks is strictly compliant with the standard. Because a thunk must call the original virtual function, in principle the normal requirements of such a call must be satisfied whenever forming a member function pointer to a virtual function. In particular, the return and parameter types must be complete, or at least must be reliably completed within the translation unit. Nothing in the standard seems to authorize requiring this at the point of forming a member pointer. The member pointer may be an ODR-use (regardless, as a virtual function the member function is implicitly ODR-used), but an ODR-use of a function does not normally require its return or parameter types to be complete.
Normally, the committee's intent might be divined by the fact that there are longstanding implementations that do use thunks. However, the implementations that use thunks (most notably, Visual C++) are often coincidentally those that maintain compatibility with a variety of pre-standardization behaviors for member pointers. Because of this, they are often understandably non-complaint around member pointers, and it is therefore difficult to point to their behavior as a guide. Many more recent implementations follow the Itanium C++ ABI and thereefore do not use thunks.
Nonetheless, we believe that there are good reasons to prefer thunks over table offsets, and we believe the standard should allow their use:
- While the use of thunks requires extra code (the thunk body) when forming a member pointer, the use of table offsets requires extra code (to dynamically detect virtuality) when calling through one. The call is more likely to be performance-sensitive.
- This extra cost of table offsets is paid whether or not the member pointer is actually to a virtual function. In fact, because member pointers can be cast to a base class type, in the ordinary case it is required even if the object's type isn't polymorphic.
- If the thunk for a particular virtual function is not unique in the program, two member pointers that logically select the same virtual function may compare unequal. However, this is permitted by the standard, which says that the result of this comparison is unspecified.
- The use of table offsets causes substantial problems for control-flow integrity schemes (CFI) because memory corruption can easily change the target of a member pointer call. The normal code-generation pattern does not restrict the range of the offset, so any member function pointer can be arbitrarily redirected.
Note that, while Visual C++ does require the return type of a virtual function to be complete in order to form a member pointeer to it, it takes advantage of a certain characteristic of its calling convention to avoid requiring parameter types to be complete. This is not portable, and the standard should require both.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment