使用clang ++,-fvisibility = hidden,typeinfo和type-erasure

时间:2013-10-21 14:02:10

标签: c++ g++ visibility elf clang++

这是我在Mac OS X上使用clang ++遇到的问题的缩小版。这是经过严格编辑以更好地反映真正的问题(第一次尝试描述问题并没有表现出问题)。

失败

我在C ++中有一大块软件,目标文件中有大量符号,所以我使用-fvisibility=hidden来保持符号表的小。众所周知,在这种情况下,必须特别注意vtable,我想我遇到了这个问题。然而,我不知道如何以一种令gcc和clang高兴的方式优雅地解决它。

考虑一个base类,其中包含一个包含一些有效负载的向下转换运算符as和一个derived类模板。对base / derived<T>用于实现类型擦除:

// foo.hh

#define API __attribute__((visibility("default")))

struct API base
{
  virtual ~base() {}

  template <typename T>
  const T& as() const
  {
    return dynamic_cast<const T&>(*this);
  }
};

template <typename T>
struct API derived: base
{};

struct payload {}; // *not* flagged as "default visibility".

API void bar(const base& b);
API void baz(const base& b);

然后我有两个不同的编译单元提供类似的服务,我可以将其近似为相同功能的两倍:从basederive<payload>下拉:

// bar.cc
#include "foo.hh"
void bar(const base& b)
{
  b.as<derived<payload>>();
}

// baz.cc
#include "foo.hh"
void baz(const base& b)
{
  b.as<derived<payload>>();
}

从这两个文件中,我构建了一个dylib。这是main函数,从dylib调用这些函数:

// main.cc
#include <stdexcept>
#include <iostream>
#include "foo.hh"

int main()
try
  {
    derived<payload> d;
    bar(d);
    baz(d);
  }
catch (std::exception& e)
  {
    std::cerr << e.what() << std::endl;
  }

最后,一个Makefile来编译和链接每个人。这里没什么特别的,当然除了-fvisibility=hidden

CXX = clang++
CXXFLAGS = -std=c++11 -fvisibility=hidden

all: main

main: main.o bar.dylib baz.dylib
    $(CXX) -o $@ $^

%.dylib: %.cc foo.hh
    $(CXX) $(CXXFLAGS) -shared -o $@ $<

%.o: %.cc foo.hh
    $(CXX) $(CXXFLAGS) -c -o $@ $<

clean:
    rm -f main main.o bar.o baz.o bar.dylib baz.dylib libba.dylib

在OS X上使用gcc(4.8)运行成功:

$ make clean && make CXX=g++-mp-4.8 && ./main 
rm -f main main.o bar.o baz.o bar.dylib baz.dylib libba.dylib
g++-mp-4.8 -std=c++11 -fvisibility=hidden -c main.cc -o main.o
g++-mp-4.8 -std=c++11 -fvisibility=hidden -shared -o bar.dylib bar.cc
g++-mp-4.8 -std=c++11 -fvisibility=hidden -shared -o baz.dylib baz.cc
g++-mp-4.8 -o main main.o bar.dylib baz.dylib

然而对于clang(3.4),这失败了:

$ make clean && make CXX=clang++-mp-3.4 && ./main
rm -f main main.o bar.o baz.o bar.dylib baz.dylib libba.dylib
clang++-mp-3.4 -std=c++11 -fvisibility=hidden -c main.cc -o main.o
clang++-mp-3.4 -std=c++11 -fvisibility=hidden -shared -o bar.dylib bar.cc
clang++-mp-3.4 -std=c++11 -fvisibility=hidden -shared -o baz.dylib baz.cc
clang++-mp-3.4 -o main main.o bar.dylib baz.dylib
std::bad_cast

但是,如果我使用

,它会起作用
struct API payload {};

但我不想暴露有效载荷类型。所以我的问题是:

  1. 为什么GCC和Clang在这里有所不同?
  2. 真的与GCC一起工作,还是我在使用未定义的行为时只是“幸运”?
  3. 我是否有办法避免让payload公开使用Clang ++?
  4. 提前致谢。

    使用不可见类型参数(EDIT)

    键入可见类模板的相等性

    我现在对正在发生的事情有了更好的了解。似乎GCC clang都要求类模板及其参数可见(在ELF意义上)以构建唯一类型。如果您按以下方式更改bar.ccbaz.cc功能:

    // bar.cc
    #include "foo.hh"
    void bar(const base& b)
    {
      std::cerr
        << "bar value: " << &typeid(b) << std::endl
        << "bar type:  " << &typeid(derived<payload>) << std::endl
        << "bar equal: " << (typeid(b) == typeid(derived<payload>)) << std::endl;
      b.as<derived<payload>>();
    }
    

    如果,您也可以看到payload

    struct API payload {};
    

    然后你会看到GCC和Clang都会成功:

    $ make clean && make CXX=g++-mp-4.8
    rm -f main main.o bar.o baz.o bar.dylib baz.dylib libba.dylib
    g++-mp-4.8 -std=c++11 -fvisibility=hidden -c -o main.o main.cc
    g++-mp-4.8 -std=c++11 -fvisibility=hidden -shared -o bar.dylib bar.cc
    g++-mp-4.8 -std=c++11 -fvisibility=hidden -shared -o baz.dylib baz.cc
    ./g++-mp-4.8 -o main main.o bar.dylib baz.dylib
    $ ./main
    bar value: 0x106785140
    bar type:  0x106785140
    bar equal: 1
    baz value: 0x106785140
    baz type:  0x106785140
    baz equal: 1
    
    $ make clean && make CXX=clang++-mp-3.4
    rm -f main main.o bar.o baz.o bar.dylib baz.dylib libba.dylib
    clang++-mp-3.4 -std=c++11 -fvisibility=hidden -c -o main.o main.cc
    clang++-mp-3.4 -std=c++11 -fvisibility=hidden -shared -o bar.dylib bar.cc
    clang++-mp-3.4 -std=c++11 -fvisibility=hidden -shared -o baz.dylib baz.cc
    clang++-mp-3.4 -o main main.o bar.dylib baz.dylib
    $ ./main
    bar value: 0x10a6d5110
    bar type:  0x10a6d5110
    bar equal: 1
    baz value: 0x10a6d5110
    baz type:  0x10a6d5110
    baz equal: 1
    

    类型相等很容易检查,实际上只有一个类型的实例化,就像它的唯一地址所见。

    但是,如果您从payload删除了可见属性:

    struct payload {};
    

    然后你得到GCC:

    $ make clean && make CXX=g++-mp-4.8
    rm -f main main.o bar.o baz.o bar.dylib baz.dylib libba.dylib
    g++-mp-4.8 -std=c++11 -fvisibility=hidden -c -o main.o main.cc
    g++-mp-4.8 -std=c++11 -fvisibility=hidden -shared -o bar.dylib bar.cc
    g++-mp-4.8 -std=c++11 -fvisibility=hidden -shared -o baz.dylib baz.cc
    g++-mp-4.8 -o main main.o bar.dylib baz.dylib
    $ ./main
    bar value: 0x10faea120
    bar type:  0x10faf1090
    bar equal: 1
    baz value: 0x10faea120
    baz type:  0x10fafb090
    baz equal: 1
    

    现在有类型的几个实例derived<payload>(如由三种不同的地址目睹),但GCC看到这些类型是相等的,和(当然)两个dynamic_cast通过。

    在clang的情况下,它是不同的:

    $ make clean && make CXX=clang++-mp-3.4
    rm -f main main.o bar.o baz.o bar.dylib baz.dylib libba.dylib
    clang++-mp-3.4 -std=c++11 -fvisibility=hidden -c -o main.o main.cc
    clang++-mp-3.4 -std=c++11 -fvisibility=hidden -shared -o bar.dylib bar.cc
    clang++-mp-3.4 -std=c++11 -fvisibility=hidden -shared -o baz.dylib baz.cc
    .clang++-mp-3.4 -o main main.o bar.dylib baz.dylib
    $ ./main
    bar value: 0x1012ae0f0
    bar type:  0x1012b3090
    bar equal: 0
    std::bad_cast
    

    还有三种类型的实例化(删除失败的dynamic_cast确实显示有三种),但这一次,它们不相等,而dynamic_cast(当然)失败。

    现在问题变成:  1.这是作者想要的两个编译器之间的差异吗?  2.如果不是,两者之间的“预期”行为是什么

    我更喜欢GCC的语义,因为它允许真正实现类型擦除,而无需公开公开包装类型。

2 个答案:

答案 0 :(得分:7)

我已经向LLVM的人报告了这个问题,first noted如果它适用于海湾合作委员会,那是因为:

  

我认为差异实际上在c ++库中。看起来像   libstdc ++更改为始终使用typeinfo名称的strcmp:

     

https://gcc.gnu.org/viewcvs/gcc?view=revision&revision=149964

     

我们应该对libc ++做同样的事吗?

对此,它是clearly answered that

  

没有。它使正确行为的代码变得困难,以解决代码问题   违反了ELF ABI。考虑一个加载插件的应用程序   RTLD_LOCAL。两个插件实现了一个名为&#34; Plugin&#34;的(隐藏)类型。该   现在GCC更改使这个完全独立的类型对所有人都相同   RTTI目的。这毫无意义。

所以我不能用Clang做我想做的事:减少已发布符号的数量。但它似乎比GCC目前的行为更为安全。太糟糕了。

答案 1 :(得分:0)

我最近遇到了这个问题,而@akim(OP)已经诊断出来了。

解决方法是编写自己的dynamic_cast_to_private_exact_type<T>或其他一些检查typeid的字符串名称。

template<class T>
struct dynamic_cast_to_exact_type_helper;
template<class T>
struct dynamic_cast_to_exact_type_helper<T*>
{
  template<class U>
  T* operator()(U* u) const {
    if (!u) return nullptr;
    auto const& uid = typeid(*u);
    auto const& tid = typeid(T);
    if (uid == tid) return static_cast<T*>(u); // shortcut
    if (uid.hash_code() != tid.hash_code()) return nullptr; // hash compare to reject faster
    if (uid.name() == tid.name()) return static_cast<T*>(u); // compare names
    return nullptr;
  }
};
template<class T>
struct dynamic_cast_to_exact_type_helper<T&>
{
  template<class U>
  T& operator()(U& u) const {
    T* r = dynamic_cast_to_exact_type<T&>{}(std::addressof(u));
    if (!r) throw std::bad_cast{};
    return *r;
  }
}
template<class T, class U>
T dynamic_cast_to_exact_type( U&& u ) {
  return dynamic_cast_to_exact_type_helper<T>{}( std::forward<U>(u) );
}

请注意,如果两个模块具有不相关的不同Foo类型,则可能存在误报。模块应将其私有类型放在匿名名称空间中以避免这种情况。

我不知道如何类似地处理中间类型,因为我们只能检查typeid比较中的确切类型,并且不能迭代类型继承树。