std::type_info

std::type_index

-D__GXX_MERGED_TYPEINFO_NAMES -rdynamic

std::type_info::operator==()

typeid

The typeid expression is an lvalue expression which refers to an object with static storage duration, of the polymorphic type const std::type_info or of some type derived from it.

std::type_info

std::vector

std::vector<const std::type_info *>

typeid

std::vector<std::type_index>

std::type_index

std::type_info

std::type_index

std::type_index

std::type_info

const std::type_info *

Is C++ typeid machinery reliable for determining uniqueness of polymorphic types robustly, efficiently, portably, and across dynamically loaded modules?

Robustness

std::type_info

std::type_index

std::type_info::operator==()

std::type_info::name()

std::type_info::hash_code()

std::type_info *

type_info::operator==

type_info

type_info::name

type_info::hash_code

Within a single execution of the program, it shall return the same value for any two type_info objects which compare equal.

O(n)

type_info::hash_code

type_info

std::type_info *

std::type_info::operator==()

type_info

typeid(Foo)

Efficiency

std::type_info

_LIBCPP_HAS_NONUNIQUE_TYPEINFO

__GXX_MERGED_TYPEINFO_NAMES

std::type_info

const char *

strcmp

_LIBCPP_HAS_NONUNIQUE_TYPEINFO

__GXX_MERGED_TYPEINFO_NAMES

std::type_info

std::type_index

_LIBCPP_HAS_NONUNIQUE_TYPEINFO

__GXX_MERGED_TYPEINFO_NAMES

// Determine whether typeinfo names for the same type are merged (in which // case comparison can just compare pointers) or not (in which case strings // must be compared), and whether comparison is to be implemented inline or // not. // We used to do inline pointer comparison by default if weak symbols // are available, but even with weak symbols sometimes names are not merged // when objects are loaded with RTLD_LOCAL, so now we always use strcmp by // default. // For ABI compatibility, we do the strcmp inline if weak symbols // are available, and out-of-line if not. Out-of-line pointer comparison // is used where the object files are to be portable to multiple systems, // some of which may not be able to use pointer comparison, but the // particular system for which libstdc++ is being built can use pointer // comparison; in particular for most ARM EABI systems, where the ABI // specifies out-of-line comparison. // The compiler's target configuration // can override the defaults by defining __GXX_TYPEINFO_EQUALITY_INLINE to // 1 or 0 to indicate whether or not comparison is inline, and // __GXX_MERGED_TYPEINFO_NAMES to 1 or 0 to indicate whether or not pointer // comparison can be used.

gcc -v

Target: x86_64-redhat-linux

x86_64-redhat-linux

type_info::operator==

g++ -std=c++11 -D__GXX_MERGED_TYPEINFO_NAMES -ldl -o test test.cpp

Dynamically Loaded Libraries

RTLD_LOCAL

dlopen

RTLD_LOCAL

This is the converse of RTLD_GLOBAL , and the default if neither flag is specified. Symbols defined in this library are not made available to resolve references in subsequently loaded libraries.

std::type_info(Foo)

RTLD_LOCAL

__GXX_TYPEINFO_EQUALITY_INLINE

std::type_info(Foo)

std::set<std::type_index>

RTLD_LOCAL

RTLD_GLOBAL

std::type_info

// Foo.h #ifndef FOO_H #define FOO_H namespace test { class Foo { virtual ~Foo() = default; }; } using namespace test; extern "C" void foo(const std::type_info &); #endif // FOO_H

// Foo.cpp (shared-library implementation) #include <iostream> #include <typeinfo> #include <cassert> #include "foo.h" void test(const std::type_info &other) { assert(other == typeid(Foo)); std::cout << "typeid equality = " << std::boolalpha << (other == typeid(Foo)) << std::endl; assert(other.hash_code() == typeid(Foo).hash_code()); std::cout << "typeid hash_code equality = " << std::boolalpha << (other.hash_code() == typeid(Foo).hash_code()) << std::endl; std::cout << "typeid name: module=" << typeid(Foo).name() << ", other=" << other.name() << std::endl; }

#include <typeinfo> #include <iostream> #include <string> #include <unistd.h> #include <dlfcn.h> #include "foo.h" int main(void) { char cwd[1024]; getcwd(cwd, sizeof(cwd)); std::string path = std::string(cwd) + "/libfoo.so"; void *handle = dlopen(path.c_str(), RTLD_GLOBAL); std::cout << "handle = " << handle << "

"; using TestFunctionType = void (*)(const std::type_info &); TestFunctionType foo_ptr = reinterpret_cast<TestFunctionType>(dlsym(handle, "test")); if(test_ptr) test_ptr(typeid(Foo)); if(handle) dlclose(handle); }

test

Foo

std::type_info

test

std::type_info

Foo

// Create libfoo.so $ clang++ -std=c++11 -D__GXX_MERGED_TYPEINFO_NAMES -fpic -shared foo.cpp -o libfoo.so // Create the main program $ clang++ -std=c++11 -D__GXX_MERGED_TYPEINFO_NAMES -ldl -o robust_typeid robust_typeid.cpp // Run $ /.robust_typeid

handle = 0x85dcf0 robust_typeid: foo.cpp:9: void test(const std::type_info &): Assertion other == typeid(Foo) failed. Aborted (core dumped)

-rdynamic

-export-dynamic

This instructs the linker to add all symbols, not only used ones, to the dynamic symbol table. This option is needed for some uses of dlopen

type_info

type_info::hash_code

-D__GXX_MERGED_TYPEINFO_NAMES -rdynamic

C++ standard library hasandto get run-time type information about a type. There are some efficiency and robustness issues in using them (especially when dynamically loaded libraries are involved.)TL;DR; Thecompiler/linker options (for both the main program and the library) generates code that uses pointer comparison inThekeyword is used to obtain a type's run-time type information. Quoting cppreference.objects can not be put inbecause they are non-copyable and non-assignable. Of course, you can have aas the object returned byhas static storage duration. You could also usecontains a pointer toand therefore, copies are possible and cheap. It's also safer to usebecause for associative containers,delegates less-then, equality, and greater-than to the underlyingobject. And that's what you want. Just usingwould do pointer comparisons. The result may be different.The real question I'm seeking an answer to isThis seems like a tall order. There's one caveat though. "Portability" for me is limited to RHEL7 Linux, MacOS 10.x, and may be Windows 10 with really latest toolchains (clang++ 7.x, g++ 8.x, Visual Studio 2017). I'm not worried about other platforms at the moment.The first step is to check iforis the same for the same type and not same for different types.We've a few things to use for comparisons:Consider. Equality comparison between twoobjects returns true for the same types and false for different types even when dynamically loaded libraries are involved. The question is how fast is it. We'll look at that a little later.The worst function for determining equality appears to be. Quoting cppreference: "No guarantees are given; in particular, the returned string can be identical for several types". I'm really bummed by that.Next is. As hashes for two different types can collide, it is useless for determining type equality. The only thing C++17 standard (n4713) says isHash calculation could also be slow as it would be typicallywhere n is the length of the mangled name. There's one implementation-specific hack though. Certain preprocessor macros (discussed below) enableto return a pointer toobject. That's super-fast. But does it provide guarantees of uniqueness? May be so.That brings us to the last option:. Ifis implemented in terms of pointer comparisons, then we might get the best of both worlds. Fast, reliablecomparisons. Is there a way? Read on...However, when shared libraries (.so on Linux, .dll on Windows) are in the picture, no such guarantee can be given. And it makes sense. As shared-library and the main program could be compiled completely independently, expecting thatis the same object in main and dynamically loaded libraries is wishful thinking. We'll tackle this issue after the next section.If you look atin libc++ and libstdc++ you will discover a couple of macros that directly determine efficiency of the comparison operators. It'sin libc++ andin libstdc++ respectively. In the respective library implementations, they control whethercomparisons are simply pointer comparisons or much more expensivecomparisons. With long names of template instantiations, the cost of-like operations could be high.If you are interested in detailed performance numbers and library code, you may want to checkout Fun with typeid() blogpost by David Holmes . The long and the short of it is that withdisabled in libc++ andenabled in libstdc++, performance ofandcomparisons is an order of magnitude better (due to just pointer comparisons).On my MacOS machine,is not defined by default. So things are good. On my RHEL7 box,is not defined. There's explanation why that's the case in libstdc++. It reads something like this.Thats' dense! I'm unclear about what merged really means in this context. What is being merged with what? Anyone?The best part is the last sentence. The standard library authors are permitting setting an otherwise internal macro (starts with __) to enable pointer comparisons. So there seems to be light at the end of the tunnel.One thing I'm not 100% sure is the keyword "target configuration". A compiler's target configuration is the machine assembly code is generated for. On my machine,prints. I.e., the resulting code is suitable for running on---a native build. I'm unclear whether the compiler and the standard library itself should be built with the same preprocessor macro. If you are curious about what, andmachines are for a compiler, see gcc configure terms and history The following invocation of the compiler seems to produce code that uses pointer comparisons inThere's another wrinkle which appears to be around dynamic loading of shared libraries. Something about "weak symbols" and. What in the world are those things?In the man pages for---a library function to load shared library files (*.so) at run-time---you will find. Quoting man pages:So if your program uses dynamically loaded libraries and the libraries rely on a globally known definition ofobject, you might be out of luck if the libraries are opened using default flags or explicitly with. Such libraries, even if compiled with, will use their own local definitions of. Obviously, if your program relies on a global unique definition, as inor some similar shenanigans, your program is likely to explode.Ok, so, I can't open the libraries withor default. I've to use. Easy.To be extra careful, I threw in a run-time check to ensure the main program and the shared-library file agree on the definition ofof Foo.The Foo header file.The Foo implementation file.And the main program (robust_typeid.cpp)The program loads libfoo.so dynamically and calls thefunction in the library. The main module passes a reference to'sobject (as observed by the main module) to function. The function checks if they agree on the uniqueness ofobject forFinally, the compiler options.It crashes with an assertion failure. Ouch!Suspicion turned to be right. Something's not right.With some google-foo, I found gcc's linker flagor. Quoting man pages:Let's try.Voilla!These two options seem to enable the best of both worlds: fast, reliablecomparisons. Additionally, thefunction returns a pointer. Does that make it non-colliding? Isreally a silver bullet? Let me know what you think. Comment on reddit/r/cpp