这是我在 Mac OS X 上使用 clang++ 时遇到的问题的缩小版本。经过认真编辑,以更好地反射(reflect)真正的问题(描述问题的第一次尝试没有表现出问题)。
失败
我有一个 C++ 软件,在目标文件中有大量符号,所以我使用 -fvisibility=hidden
保持我的符号表很小。众所周知,在这种情况下,必须特别注意 vtables,我想我面临这个问题。但是,我不知道如何以一种让 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);
然后我有两个提供类似服务的不同编译单元,我可以将其近似为相同功能的两倍:从 base
向下转换至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 调用这些函数:
// 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 {};
但我不想暴露负载类型。所以我的问题是:
- 为什么 GCC 和 Clang 在这里不同?
- 是真的在使用 GCC,还是我只是“幸运”地使用了未定义的行为?
- 我有办法避免
payload
使用 Clang++ 上市?
提前致谢。
可见类模板与不可见类型参数的类型相等(编辑)
我现在对正在发生的事情有了更好的了解。似乎 GCC 和 clang 都要求类模板及其参数都是可见的(在 ELF 意义上)以构建唯一类型。如果您更改 bar.cc
和 baz.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 的语义,因为它允许真正实现类型删除,而无需公开公开封装的类型。
最佳答案
我已经向 LLVM 的人报告了这个,它是 first noted如果它适用于 GCC,那是因为:
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
Should we do the same with libc++?
对此,它是 clearly answered that :
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 的当前行为更理智。太糟糕了。
关于c++ - 使用 clang++、-fvisibility=hidden、typeinfo 和 type-erasure,我们在Stack Overflow上找到一个类似的问题: https://stackoverflow.com/questions/19496643/