Использование clang++, -fvisibility = hidden и typeinfo, а также стирание типа
Это уменьшенная версия проблемы, с которой я столкнулся с clang++ в Mac OS X. Это было серьезно отредактировано, чтобы лучше отразить подлинную проблему (первая попытка описать проблему не показала проблему).
Неисправность
У меня есть эта большая часть программного обеспечения на С++ с большим набором символов в объектных файлах, поэтому я использую -fvisibility=hidden
, чтобы мои таблицы символов были маленькими. Хорошо известно, что в таком случае нужно обратить особое внимание на vtables, и я полагаю, что я столкнулся с этой проблемой. Однако я не знаю, как обращаться с ним элегантно таким образом, чтобы нравиться как gcc, так и clang.
Рассмотрим класс base
, в котором используется оператор down-casting, 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 [email protected] $^
%.dylib: %.cc foo.hh
$(CXX) $(CXXFLAGS) -shared -o [email protected] $<
%.o: %.cc foo.hh
$(CXX) $(CXXFLAGS) -c -o [email protected] $<
clean:
rm -f main main.o bar.o baz.o bar.dylib baz.dylib libba.dylib
Запуск выполняется с помощью gcc (4.8) на 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) это не удается:
$ 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, или я просто "повезло" в моем использовании поведения undefined?
- Есть ли у меня способ избежать того, чтобы
payload
публиковаться с Clang++?
Спасибо заранее.
Тип равенства видимых шаблонов классов с параметрами невидимого типа (EDIT)
Теперь я лучше понимаю, что происходит. Похоже, что и 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
pass.
В случае с 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, так как она позволяет реально реализовать стирание стилей без необходимости публично публиковать обернутые типы.
Ответы
Ответ 1
Я сообщил об этом людям из LLVM, и сначала было отмечено, что, если это работает в случае GCC, это потому, что:
Я думаю, что разница на самом деле в библиотеке c++. Похоже, что libstd c++ изменен, чтобы всегда использовать strcmp имен typeinfo:
https://gcc.gnu.org/viewcvs/gcc?view=revision&revision=149964
Должны ли мы сделать то же самое с lib c++?
На это было четко сказано, что:
Нет. Он пессимизирует код с правильным поведением, чтобы обойти код, который нарушает ABI ELF. Рассмотрим приложение, которое загружает плагины с RTLD_LOCAL. Два плагина реализуют (скрытый) тип под названием "Плагин". Изменение GCC теперь делает эти совершенно разные типы идентичными для всех целей RTTI. Это не имеет никакого смысла.
Поэтому я не могу делать то, что я хочу с Clang: уменьшить количество опубликованных символов. Но, похоже, это разумнее, чем нынешнее поведение GCC. Очень плохо.
Ответ 2
Недавно я столкнулся с этой проблемой, и @akim (OP) поставил диагноз.
Обходной путь заключается в том, чтобы написать собственный dynamic_cast_to_private_exact_type<T>
или somesuch, который проверяет имя строки 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
comparsion и не можем перебирать дерево типа наследования.