Это уменьшенная версия проблемы, с которой я столкнулся с clang++ в Mac OS X. Это было серьезно отредактировано, чтобы лучше отразить реальную проблему (первая попытка описать проблему не демонстрировала проблему).
Провал
У меня есть большая программа на 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
Запуск выполняется успешно с 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, или мне просто "повезло" в использовании неопределенного поведения?
- есть ли у меня способ избежать публикации
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
проходят.
В случае с 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
. - person akim   schedule 21.10.2013API
кderived
, чтобы он работал правильно. Однако это не работает в моей реальной проблеме, и я пока не знаю, в чем разница между полномасштабной проблемой и этой небольшой абстракцией. - person akim   schedule 21.10.2013derived
общедоступным) больше недостаточно. - person akim   schedule 21.10.2013libc++
, а неclang++
,clang++
сlibstdc++
, скорее всего, будет работать в этом сценарии. - person Predelnik   schedule 15.01.2019