Erasures and bloat vs. ABI stability

Hi guys,

We want to share implementation details between std::function and its new cousins which we are designing. A program which targets a class foo by std::function<void()> shouldn’t pay the same price a second time when it initializes a std::unique/movable_function<void()> with a foo as well. However, we also don’t want to use exactly the same erasure class for both wrappers, because then unique_function would instantiate copy constructors it cannot use, representing a different kind of bloat.

The solution is to share some functions between similar templates. This might violate libc++’s ABI standards, or it might not. Internal functions, including erasure methods, are declared using this macro:

#define _LIBCPP_INLINE_VISIBILITY attribute ((always_inline))

The idea is that it’s dangerous for a function to be exported from a DLL, then vanish in a subsequent version at the whim of the inliner. A function which is always inlined will never be visible in the first place. This stabilizes not only libc++.dylib, but also user libraries with respect to the standard library.

The danger, but not the solution, also applies to vtables. If libc++ (or another library) uses a polymorphic class, then it’s committed to exporting it forever. The erasures of std::function are implemented using vtables, but this isn’t the only possible approach. libstdc++ uses switch statements instead.

Introducing new-but-similar specializations to the the vtable-polymorphic erasure class is making me confused.

  1. Is it OK at all that shared libraries tend to export standard erasures? Or is this all a giant bug requiring a ground-up rewrite? (I’ve not dug into the history.)
  2. If OK, should shared functions dominated by the vtables of several specializations be marked noinline? (Such a function is unreachable except through a vtable, which is already exported.) Is there some macro or protocol for this?
  3. I’m spending this effort optimizing a case without first analyzing it. Has libc++ already decided whether to care about std::function overhead, or should I just make fully-independent specializations and be through with it? Is there a known, quantified margin of headroom?

Regarding #2, why does the virtual function std::__function::__base::~__base have inline visibility? Yes, it’s called directly by the derived class destructor, but that is only accessible by a vtable. Is there some potential ABI error if it omitted the visibility spec, or is it just erring on the side of caution? (All the other virtual functions omit the spec.)

  • Thanks much,
    David

Marking the class std::__function::__func as _LIBCPP_HIDDEN looks like an easier alternative. I’m not sure I’m testing the idea correctly, but it seems to work. Setting this attribute causes different shared libraries to see different classes. Armed with this, erasure specializations cleanly hide their internal workings from the ABI, exposing only the base-class vtable layout.

Already, without the attribute, each shared library internally refers to its own vaguely-linked copies of the vtables, which point to local, vague copies of the functions. The issue seems subtler than I made it out to be. An executable won’t cease to load when a specialization disappears from a shared library, nor could an object ever see functions called from the wrong vtable. The difference is maybe only that instances claim to be of the same dynamic type according to typeid, dynamic_cast, and throw, but that doesn’t ever actually come up. I might be missing something though. (I’ve disproven my theory of why the destructor is _LIBCPP_INLINE_VISIBLITY but the other virtual members aren’t, so that’s once again a mystery.)