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

aki*_*kim 16 c++ visibility g++ elf clang++

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

失败

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

考虑一个base具有向下转换运算符asderived类,以及包含一些有效负载的类模板.pair 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);
Run Code Online (Sandbox Code Playgroud)

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

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

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

从这两个文件中,我构建了一个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;
  }
Run Code Online (Sandbox Code Playgroud)

最后,一个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
Run Code Online (Sandbox Code Playgroud)

在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
Run Code Online (Sandbox Code Playgroud)

但是对于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
Run Code Online (Sandbox Code Playgroud)

但是,如果我使用它可行

struct API payload {};
Run Code Online (Sandbox Code Playgroud)

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

  1. 为什么GCC和Clang在这里有所不同?
  2. 它是真的与GCC合作,还是我在使用未定义的行为时只是"幸运"?
  3. 我是否有办法避免payload使用Clang ++上市?

提前致谢.

使用不可见类型参数键入可见类模板的相等性(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>>();
}
Run Code Online (Sandbox Code Playgroud)

如果你让payload可见太:

struct API payload {};
Run Code Online (Sandbox Code Playgroud)

然后你会看到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
Run Code Online (Sandbox Code Playgroud)

类型相等很容易检查,实际上只有一个类型的实例化,如其唯一地址所见.

但是,如果从payload以下位置删除visible属性:

struct payload {};
Run Code Online (Sandbox Code Playgroud)

然后你得到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
Run Code Online (Sandbox Code Playgroud)

现在有几种类型的实例化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
Run Code Online (Sandbox Code Playgroud)

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

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

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

aki*_*kim 7

我已经向LLVM的人们报告了这个问题,并且首先注意到如果它适用于GCC,那是因为:

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

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

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

对此,明确回答:

不会.它会严重正确地运行代码以解决违反ELF ABI的代码.考虑使用RTLD_LOCAL加载插件的应用程序.两个插件实现了一个名为"插件"的(隐藏)类型.现在,GCC的变化使得这个完全独立的类型对于所有RTTI目的都是相同的.这毫无意义.

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