Использование clang++, -fvisibility=hidden, typeinfo и type-erasure

Это уменьшенная версия проблемы, с которой я столкнулся с 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 {};

но я не хочу раскрывать тип полезной нагрузки. Итак, мои вопросы:

  1. почему GCC и Clang здесь разные?
  2. это действительно работает с GCC, или мне просто "повезло" в использовании неопределенного поведения?
  3. есть ли у меня способ избежать публикации 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, так как она позволяет действительно реализовать стирание типов без необходимости публиковать обернутые типы.


person akim    schedule 21.10.2013    source источник
comment
Что произойдет, если вы измените dynamic_cast на static_cast?   -  person doron    schedule 21.10.2013
comment
static_cast работает, и в моем случае мне не нужен dynamic_cast, так как в as передаются только допустимые параметры. Тем не менее, мне нравится быть дважды проверенным компилятором/средой выполнения, и использование static_cast для меня похоже на готовность к продукту, а dynamic_cast — для отладки. Так что я действительно хочу использовать dynamic_cast.   -  person akim    schedule 21.10.2013
comment
FWIW, в этом примере достаточно добавить API к derived, чтобы он работал правильно. Однако это не работает в моей реальной проблеме, и я пока не знаю, в чем разница между полномасштабной проблемой и этой небольшой абстракцией.   -  person akim    schedule 21.10.2013
comment
Я отредактировал первоначальный вопрос, чтобы лучше отразить проблему, поэтому моего предыдущего комментария (сделать derived общедоступным) больше недостаточно.   -  person akim    schedule 21.10.2013
comment
Я думаю, что это как-то связано с тем, как и где создаются экземпляры шаблонов. dynamic_cast использует RTTI из полезной нагрузки, которая, вероятно, недоступна (по какой-то причине) в модуле компиляции, где она требуется. У GCC и Clang вполне могут быть разные способы сделать это.   -  person doron    schedule 21.10.2013
comment
1. Расширение файла сообщает компилятору, какой тип кода содержится внутри. 2. XCode не поддерживает стандарт c11, несмотря на то, что они утверждают. Я лично зарегистрировал несколько ошибок, которые, по словам Apple, они не собираются исправлять. Надеюсь это поможет.   -  person Dan    schedule 21.02.2014
comment
Хотел бы отметить, что вопрос касается использования libc++, а не clang++, clang++ с libstdc++, скорее всего, будет работать в этом сценарии.   -  person Predelnik    schedule 15.01.2019


Ответы (2)


Я сообщил об этом людям из LLVM, и это было первое. отметил, что если это работает в случае с GCC, то это потому, что:

Я думаю, что разница на самом деле в библиотеке С++. Похоже, что libstdc++ изменился, чтобы всегда использовать strcmp имен typeinfo:

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

Должны ли мы сделать то же самое с libc++?

На это он четко ответил, что:

Нет. Он пессимизирует корректно работающий код, чтобы обойти код, нарушающий ELF ABI. Рассмотрим приложение, которое загружает плагины с RTLD_LOCAL. Два плагина реализуют (скрытый) тип под названием «Плагин». Изменение GCC теперь делает эти совершенно отдельные типы идентичными для всех целей RTTI. В этом нет никакого смысла.

Поэтому я не могу сделать с Clang то, что хочу: уменьшить количество публикуемых символов. Но это кажется более разумным, чем текущее поведение GCC. Очень жаль.

person akim    schedule 03.03.2015

Недавно я столкнулся с этой проблемой, и @akim (ОП) диагностировал ее.

Обходной путь - написать свой собственный 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 сравнении и не можем перебирать дерево наследования типов.

person Yakk - Adam Nevraumont    schedule 08.08.2017