[cxx-abi-dev] Uncallable constructor variants

John McCall rjmccall at apple.com
Fri Apr 1 18:27:04 UTC 2016


> On Apr 1, 2016, at 8:33 AM, Jason Merrill <jason at redhat.com> wrote:
> On 04/01/2016 10:47 AM, Nathan Sidwell wrote:
>> On 04/01/16 10:36, Jason Merrill wrote:
>>> On 04/01/2016 10:33 AM, Nathan Sidwell wrote:
>>>> On 04/01/16 10:15, Jason Merrill wrote:
>>>>> The base object constructor can never be called for a final class,
>>>>> nor can the
>>>>> complete object constructor for an abstract one.  Does anyone see an
>>>>> ABI problem
>>>>> with omitting these variants?
>>>> 
>>>> Presumably the same applies to the corresponding destructors? (With any
>>>> deleting-dtor behaving the same as the complete-dtor)
>>> 
>>> Yes, except that virtual complete/deleting destructors always need to be
>>> emitted, to satisfy vtable references.
>> 
>> They could behave as pure virtual, and have the vtable reference
>> __cxa_pure_virtual.  Though that would be an ABI change, in that the
>> vtable emission might be in a different TU to the virtual destructor
>> (non)-emitter.
>> 
>> I suppose the destructors could simply be jumps to __cxa_pure_virtual,
>> without perturbing the ABI.  If the destructors are emitted in the same
>> TU as the vtable, an implementation would be free to reference
>> __cxa_pure_virtual directly.
> 
> Right, the implementation just needs to define the symbol so as not to break existing references.

Right.  We should probably recommend a variant of this rule as mandatory
for future targets that adopt the generic ABI:

  - never emit a complete-object constructor or destructor for an abstract class
  - never emit a base object constructor or destructor for a final class
  - emit virtual destructor slots for abstract class v-tables as __cxa_pure_virtual

 ***

Related question that comes to mind:

Mandating the existence of both a complete-object and a base-subobject variant
is frequently wasteful.  At the very least, it inflates the symbol table, which in C++
can represent a pretty significant portion of the binary size of a library.  It can also
inflate code size if the compiler is unable to emit one as simply an alias to the other.

We can classify classes into three interesting cases:

1. Classes with virtual bases.  We obviously need both variants here, because (a)
they can have semantic differences (although they don't necessarily) and (b) they
have different signatures because of the VTT argument.

There's also a potential "devirtualization" optimization for virtual base accesses
within the ctor/dtor body, since the complete object variant can use a static offset
to the virtual bases of "this".  Note that there are no additional devirtualization
opportunities for virtual calls because the C++ model resolves these to the final
overriders within the current class in either case; that is, both variants should be
able to fully devirtualize calls on "this" within the body.

2. Non-polymorphic classes without virtual bases.  I cannot think of any way in
which emitting symbols for both variants could ever be useful in this case.  It
does nothing but inflate the symbol table.

3. Polymorphic classes without virtual bases.  The two variants are equivalent in
both semantics and signature.  Furthermore, as discussed above, there are no
additional devirtualization opportunities in the complete-object variant because
virtual dispatch resolves to the current class in either case, and there no virtual
bases to adjust to.

The only optimization opportunity I can see here is that the base variant can avoid
initializing any v-table pointers if it can prove that they'll be unused before returning.
Doing this could be an important optimization for deep class hierarchies with
many opaquely-defined constructors.  However, it creates a problem for inline
constructor definitions due to a mismatch in assumptions between translation units.

For example, suppose that translation unit A emits a complete object constructor
as either an alias or a tail-call of the base-subobject constructor, which it then
emits in a way that always initializes the v-table pointer.  Translation unit B, on
the other hand, recognizes that the base-subobject constructor can avoid initializing
the v-table pointer and instead initializes it only in the complete-object constructor.
If the linker might choose the implementation from B, it is necessary to somehow
prevent translation unit A from acting on its "knowledge" that the base-subobject
constructor always initializes the v-table pointer by emitting a complete-object
construction as a direct call to the base-subobject constructor.  That seems... tricky
to accomplish in general.

 ***

The problem in case #3 seems like something we need to settle even for current
users of the ABI.  Maybe this is already solved by COMDATs on ELF, although
I don't know how a compiler is expected to reason about them effectively.

I would recommend that future targets adopt a rule that base-subobject variants
aren't emitted for at least case #2.  (Note that there is no conflict between this
and the recommended abstract-class rule because non-polymorphic classes
cannot be abstract.)

John.


More information about the cxx-abi-dev mailing list