Vittorio Romeo
Vittorio Romeo

Reputation: 93274

Why does `std::make_shared` perform two separate allocations with `-fno-rtti`?

#include <memory>
struct foo { };
int main() { std::make_shared<foo>(); }

The asssembly generated by both g++7 and clang++5 with -fno-exceptions -Ofast for the code above:

This can be easily verified on gcc.godbolt.org (clang++5 version):

screenshot of the above godbolt link with highlighed operator new calls

Why is this happening? Why does disabling RTTI prevent make_shared from unifying the object and control block allocations?

Upvotes: 25

Views: 1656

Answers (3)

WhiZTiM
WhiZTiM

Reputation: 21576

Naturally, std::shared_ptr will be implemented with the assumption of the compiler supporting rtti. But it can be implemented without it. See shared_ptr without RTTI? .

Taking a cue from this old GCC's libstdc++ #42019 bug. We can see that Jonathan Wakely added a fix to make this possible without RTTI.

In GCC's libstdc++, std::make_shared uses the services of std::allocated_shared which uses a non-standard constructor(as seen in the code, reproduced below).

As seen in this patch, from line 753, you can see that getting the default deleter simply requires using the services of typeid if RTTI is enabled, otherwise, it requires a separate allocation that doesn't depend on RTTI.

EDIT: 9 - May -2017: removed copyrighted code previously posted here

I haven't investigated libcxx, but I want to believe they did similar thing....

Upvotes: 11

Yakk - Adam Nevraumont
Yakk - Adam Nevraumont

Reputation: 275385

No good reason. This looks like a QoI issue in libstdc++.

Using clang 4.0, libc++ does not have this issue., while libstdc++ does.

The libstdc++ implementation with RTTI relies on get_deleter:

void* __p = _M_refcount._M_get_deleter(typeid(__tag));
                  _M_ptr = static_cast<_Tp*>(__p);
                  __enable_shared_from_this_helper(_M_refcount, _M_ptr, _M_ptr);
_M_ptr = static_cast<_Tp*>(__p);

and in general, get_deleter isn't possible to implement without RTTI.

It appears that it is using the deleters position and the tag to store the T in this implementation.

Basically, the RTTI version used get_deleter. get_deleter relied on RTTI. Getting make_shared to work without RTTI required rewriting it, and they took an easy route that caused it to do two allocations.

make_shared unifies the T and reference counting blocks. I suppose with both variable sized deleters and variable sized T things get nasty, so they reused the deleter's variable sized block to store the T.

A modified (internal) get_deleter that did not do RTTI and returned a void* might be enough to do what they need from this deleter; but possibly not.

Upvotes: 6

Useless
Useless

Reputation: 67723

Why does disabling RTTI prevent make_shared from unifying the object and control block allocations?

You can see from the assembler (just pasting the text is really preferable to both linking and to taking pictures of it) that the unified version doesn't allocate a simple foo but an std::_Sp_counted_ptr_inplace, and further that that type has a vtable (recall it needs a virtual destructor in general, to cope with custom deleters)

mov QWORD PTR [rax], OFFSET FLAT:
  vtable for
  std::_Sp_counted_ptr_inplace<foo, std::allocator<foo>,
  (__gnu_cxx::_Lock_policy)2>+16

If you disable RTTI, it can't generate the inplace counted pointer because that needs to be virtual.

Note that the non-inplace version still refers to a vtable, but it seems to just be storing the de-virtualized destructor address directly.

Upvotes: 12

Related Questions