22

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

失败

我在 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);

然后我有两个提供类似服务的不同编译单元,我可以将其近似为相同功能的两倍:从向下转换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++ 公开吗?

提前致谢。

具有不可见类型参数的可见类模板的类型相等(编辑)

我现在对正在发生的事情有了更好的了解。似乎 GCCclang 都要求类模板和它的参数都是可见的(在 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

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

但是,如果您从以下位置删除 visible 属性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 的语义,因为它允许真正实现类型擦除,而无需公开公开包装的类型。

4

2 回答 2

10

我已经向 LLVM 的人报告了这一点,首先注意到如果它适用于 GCC,那是因为:

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

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

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

对此,明确的回答是

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

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

于 2015-03-03T08:46:31.810 回答
1

我最近遇到了这个问题,@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而不能遍历类型继承树。

于 2017-08-08T14:38:11.250 回答