失败原因:
我有一个大型C++软件,其中目标文件中有大量符号,因此我使用“-fvisibility=hidden”来保持我的符号表小。众所周知,在这种情况下,人们必须特别注意虚函数表,而我想我面临了这个问题。但是,我不知道如何以一种既能让gcc又能让clang满意的方式优雅地解决它。
考虑一个具有向下转换运算符“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的语义,因为它允许真正实现类型抹除,而不需要公开包装类型。
static_cast
能够正常工作,在我的情况下,我并不真正需要dynamic_cast
,因为只有有效的参数被传递给了as
。然而,我喜欢编译器/运行时进行双重检查,使用static_cast
对我来说就像是产品准备好了,而dynamic_cast
则用于调试。所以我真的想使用dynamic_cast
。 - akimderived
公开)不再适用。 - akim