7

I have a C++ framework being built in the latest version of Xcode (9.4.1 at the time of writing) that I am using from Objective-C++ code, again within Xcode. I need to perform a dynamic_cast from one pointer type to another. However, the dynamic_cast is only working from a Debug build and is not working from a Release build. Is there something I'm missing or understanding about how dynamic_cast works here within Objective-C++ that makes this sample fail?

C++ Framework

TestClass.hpp

class Parent {
    public:
    // https://stackoverflow.com/a/8470002/3938401
    // must have at least 1 virtual function for RTTI
    virtual ~Parent();

    Parent() {}
};

class Child : public Parent {
public:
    // if you put the implementation for this func 
    // in the header, everything works.
    static Child* createRawPtr(); 
};

TestClass.cpp

#include "TestClass.hpp"

Parent::~Parent() {}

Child* Child::createRawPtr() {
    return new Child;
}

Objective-C++ Command Line App

main.mm

#import <Foundation/Foundation.h>
#import <TestCastCPP/TestClass.hpp>

int main(int argc, const char * argv[]) {
    @autoreleasepool {
        Parent *parentPtr = Child::createRawPtr();
        Child *child = dynamic_cast<Child*>(parentPtr);
        NSLog(@"Was the cast successful? %s", child != nullptr ? "True" : "False");
    }
    return 0;
}

In both Debug and Release, I expect this code to print "True". However, in reality, Release mode prints "False". As a smoke test, the dynamic_cast at this SO post works just fine.

Interestingly, the same sort of code works from a C++ command line application, again within Xcode. I have tried disabling the optimizer in Release mode, but this did not seem to fix the problem.

I have a sample project up on GitHub here. Remember to compile it in Release to see the reason for my question. I've included the TestCast scheme for Objective-C++, and the TestCastCPP scheme for the straight C++.

rmaddy
  • 314,917
  • 42
  • 532
  • 579
Deadpikle
  • 356
  • 6
  • 22
  • 1
    Does it have the same problem if you provide an explicit virtual Child destructor, and put that definition right after the Parent destructor? (Because https://stackoverflow.com/a/8470002/3938401 also applies to the Child class.) – Eljay Jun 19 '18 at 14:14
  • @Eljay Good question. I tried out your suggestion (declared Child destructor in header with `override`, implementation in cpp just like Parent destructor), and it worked! (Edit: I see your edit re: RTTI. Hm.) – Deadpikle Jun 19 '18 at 14:18
  • @Eljay I tried this out in my work project (where the issue came up originally), and your solution worked. The RTTI reasoning makes sense, although I'm not sure why it doesn't work from Objective-C++ specifically. Please post it as an answer so that I can accept it. Thank you! – Deadpikle Jun 19 '18 at 14:24

1 Answers1

2

It is hard to know the compiler specifics, since how a compiler does RTTI has some flexibility (i.e., not specified in great detail by the specification).

In this case, since Child class did not have any virtual functions defined, I suspect the compiler had emitted RTTI with every translation unit for the Child class.

When the framework was linked, and when the executable was linked, each had their own Child RTTI information, since each translation unit emitted its own RTTI.

The Parent link of the one did not match the Parent link of the other, I suspect, so they did not have the same Parent pointer and those things were not "fixed up" by the dynamic loader. (The dynamic_cast<Child*> basically walks the parent pointer chain until it finds a match by pointer value, not by RTTI value.)

If you looked at nm -g TestCast | c++filt dumps of the app and of the framework, you can see the RTTI blocks. Disassembling them, I think the Child RTTI was already resolved in both situations to their own Parent RTTI.

Why did it work for DEBUG, but not RELEASE? Probably, one of the release optimizations was dead-code stripping of external linkage symbols based on usage. So the dynamic loader (dyld) for DEBUG was able to resolve the symbols, but the RELEASE build one-or-more symbols was already internally resolved.

There's probably a way to indicate an "unused" symbol for the RTTI should be retained and exported, which will vary by compiler/linker. But that's more bother than providing an explicit "first virtual function" (such as the virtual Child destructor) which avoids the problem.

Eljay
  • 4,648
  • 3
  • 16
  • 27