使用 clang++、-fvisibility=hidden、typeinfo 和 type-erasure [英] Using clang++, -fvisibility=hidden, and typeinfo, and type-erasure

查看:45
本文介绍了使用 clang++、-fvisibility=hidden、typeinfo 和 type-erasure的处理方法,对大家解决问题具有一定的参考价值,需要的朋友们下面随着小编来一起学习吧!

问题描述

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

This is a scaled down version of a problem I am facing with clang++ on Mac OS X. This was seriously edited to better reflect the genuine problem (the first attempt to describe the issue was not exhibiting the problem).

我在 C++ 中有这么大的软件,在目标文件中有大量符号,所以我使用 -fvisibility=hidden 来保持我的符号表很小.众所周知,在这种情况下,必须特别注意 vtables,我想我面临这个问题.但是,我不知道如何以一种让 gcc 和 clang 都满意的方式优雅地解决它.

I have this big piece of software in C++ with a large set of symbols in the object files, so I'm using -fvisibility=hidden to keep my symbol tables small. It is well known that in such a case one must pay extra attention to the vtables, and I suppose I face this problem. I don't know however, how to address it elegantly in a way that pleases both gcc and clang.

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

Consider a base class which features a down-casting operator, as, and a derived class template, that contains some payload. The pair base/derived<T> is used to implement type-erasure:

// 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);

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

Then I have two different compilation units that provide a similar service, which I can approximate as twice the same feature: down-casting from base to derive<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 调用这些函数:

From these two files, I build a dylib. Here is the main function, calling these functions from the 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.

Finally, a Makefile to compile and link everybody. Nothing special here, except, of course, -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) 运行成功:

The run succeeds with gcc (4.8) on OS X:

$ 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),这会失败:

However with clang (3.4), this fails:

$ 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 {};

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

but I do not want to expose the payload type. So my questions are:

  1. 为什么 GCC 和 Clang 在这里不同?
  2. 真的在使用 GCC,还是我只是幸运"地使用了未定义的行为?
  3. 我有办法避免使用 Clang++ 使 payload 公开吗?
  1. why are GCC and Clang different here?
  2. is it really working with GCC, or I was just "lucky" in my use of undefined behavior?
  3. do I have a means to avoid making payload go public with Clang++?

提前致谢.

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

I have now a better understanding of what is happening. It is appears that both GCC and clang require both the class template and its parameter to be visible (in the ELF sense) to build a unique type. If you change the bar.cc and baz.cc functions as follows:

// 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>>();
}

并且 if 你也让 payload 可见:

and if you make payload visible too:

struct API payload {};

那么你会看到 GCC 和 Clang 都会成功:

then you will see that both GCC and Clang will succeed:

$ 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

类型相等性很容易检查,实际上只有一个类型的实例化,正如其唯一地址所证明的那样.

Type equality is easy to check, there is actually a single instantiation of the type, as witnessed by its unique address.

但是,如果您从 payload 中删除 visible 属性:

However, if you remove the visible attribute from 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 类型的实例化(由三个不同的地址证明),但 GCC 认为这些类型是相等的,并且(当然)这两个 dynamic_cast 通过.

Now there are several instantiation of the type derived<payload> (as witnessed by the three different addresses), but GCC sees these types are equal, and (of course) the two dynamic_cast pass.

在clang的情况下就不同了:

In the case of clang, it's different:

$ 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(当然)失败了.

There are also three instantiations of the type (removing the failing dynamic_cast does show that there are three), but this time, they are not equal, and the dynamic_cast (of course) fails.

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

Now the question turns into: 1. is this difference between both compilers wanted by their authors 2. if not, what is "expected" behavior between both

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

I prefer GCC's semantics, as it allows to really implement type-erasure without any need to expose publicly the wrapped types.

推荐答案

我已经向 LLVM 的人报告了这个问题,它是 首先注意到如果它在 GCC 的情况下有效,那是因为:

I had reported this to the people from LLVM, and it was first noted that if it works in the case of GCC, it's because:

我认为区别实际上在于 c++ 库.看起来像libstdc++ 更改为始终使用类型信息名称的 strcmp:

I think the difference is actually in the c++ library. It looks like libstdc++ changed to always use strcmp of the typeinfo names:

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

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

Should we do the same with libc++?

对此,明确回答了:

没有.它使正确行为的代码悲观以解决以下代码违反 ELF ABI.考虑一个加载插件的应用程序RTLD_LOCAL.两个插件实现了一种称为插件"的(隐藏)类型.这GCC 更改现在使这种完全独立的类型对所有人都相同RTTI 目的.这根本没有意义.

No. It pessimizes correctly behaving code to work around code that violates the ELF ABI. Consider an application that loads plugins with RTLD_LOCAL. Two plugins implement a (hidden) type called "Plugin". The GCC change now makes this completely separate types identical for all RTTI purposes. That makes no sense at all.

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

So I can't do what I want with Clang: reduce the number of published symbols. But it appears to be saner than the current behavior of GCC. Too bad.

这篇关于使用 clang++、-fvisibility=hidden、typeinfo 和 type-erasure的文章就介绍到这了,希望我们推荐的答案对大家有所帮助,也希望大家多多支持IT屋!

查看全文
登录 关闭
扫码关注1秒登录
发送“验证码”获取 | 15天全站免登陆