XCodeでハマったこと

こんにちは、株式会社CFlatです。

XCodeにて、C/C++ Library(Type=Dynamic)と、Command Line Tool(Type=C++)の、2つのプロジェクトを用意します。そしてそれぞれに次のようなコードを書いて、デバッグビルドします。

A.h (project1)

#include <cstdio>
#include <string>

class Root
{
public :
  Root() {}
  virtual ~Root() {}

  static void print(const std::string& label, const Root& t) ;
} ;

class Base : public Root
{
  public :
  Base() {}
  virtual ~Base() {}
} ;

class Derived : public Base
{
  public :
  Derived() {}
  virtual ~Derived() {}
} ;

class Other : public Root
{
public :
  Other() {}
  virtual ~Other() {}
} ;

A.cpp (project1)

#include "A.h"

void Root::print(const std::string& label, const Root& t) {
  std::printf("%s*:%p, Base*:%p\n", label.c_str(), &t, dynamic_cast<const Base*>(&t)) ;
}

main.cpp (project2)

#include "A.h"

int main(int argc, const char * argv[])
{
  Base::print("Root   ", Root()) ;  // Root   *:0x7fff5fbff808, Base*:0x0
  Base::print("Base   ", Base()) ;  // Base   *:0x7fff5fbff7f0, Base*:0x7fff5fbff7f0
  Base::print("Derived", Derived()) ;  // Derived*:0x7fff5fbff7d8, Base*:0x7fff5fbff7d8
  Base::print("Other  ", Other()) ;  // Other  *:0x7fff5fbff7c0, Base*:0x0
  return 0;
}

Root→Base→Derivedという継承関係があるので、BaseとDerivedは当然ながらBaseにdynamic_castできます。RootのインスタンスとOtherのインスタンスは、Baseではないのでnullptrになります。ここまでは当たり前。

次に、Edit Scheme...→Build ConfigurationをReleaseとして同じコードを実行すると、出力は次のようになります。


Root *:0x7fff5fbff7d0, Base*:0x0
Base *:0x7fff5fbff7b8, Base*:0x0
Derived *:0x7fff5fbff7a0, Base*:0x0
Other *:0x7fff5fbff788, Base*:0x0

なんと唐突に、dynamic_castが全てnullptrを返すようになってしまいます!

実はこの現象、print()メンバ関数をcppではなくヘッダファイル内にinlineで定義した場合には、起こらないのです。
どうやらRelaseビルドでは、ライブラリ側のRTTI(実行時型情報)が正しく取れてこない模様。ダイナミックリンクするライブラリでは、dynamic_castを使えないのでしょうか?

もちろんそんな事はなくて、次のようなコードでクラス定義を挟み込んでやれば、挟まれた部分のクラスのRTTIも、ライブラリ外に公開されてくれます。

#pragma GCC visibility push(default)
// ここにクラス定義
#pragma GCC visibility pop

いや、このpragmaの存在自体は知ってたんですが、まさかそんな意味があったとは……。