2017-01-25 8 views
3

У меня есть некоторые проблемы с безопасностью преобразований типов. Я разрабатываю абстрактный интерфейс, ведь это будет поддерживаться плагинами, экспортирующими объект, ориентированный на объект C ABI , то есть указатели на объекты и функции стиля С формы func(void *this, ...), а не на функции элемента стиля C++, они затем будут упакованы в структуру, представляющую реализацию объектов. Однако некоторые из моих базовых фреймворков используют несколько виртуальных наследований.Пропустить объект C++ (с возможным множественным виртуальным наследованием) через C ABI через указатель void

Упрощенный пример

class A 
{ 
    public: 
     virtual void doA() 
} 

class B 
{ 
    public: 
     virtual void doB() 
} 

class C : public A, public B 
{ 
    public: 
     virtual void doA() 
     virtual void doB() 
} 

struct impA 
{ 
    (*doA)(void *self); 
} 

struct impB 
{ 
    (*doB)(void *self); 
} 

struct impC 
{ 
    (*doA)(void *self); 
    (*doB)(void *self); 
} 

void * AfromC(void *v) { 
    C*c = reinterpret_cast<C*>(v); // Known to be C* type 
    return static_cast<void*>(static_cast<A*>(c)); // method 1 
    return reinterpret_cast<void*>(static_cast<A*>(c)); // method 2 

    //method 3 & 4 
    C** c = static_cast<C**>(v); // Known to be C* type 
    return static_cast<void*>(&static_cast<A*>(*c)); // method 3 
    return static_cast<void*>(static_cast<A**>(c)); // method 4 
} 

/////////// main code 

class A 
{ 
    public: 
     void doA() { imp.doA(self); } 
    private: 
     impA imp; 
     void *self; 
} 

class B 
{ 
    public: 
     void doB() { imp.doB(self); } 
    private: 
     impB imp; 
     void *self; 
} 

Рассмотрим AfromC, у меня есть 4 возможных методов получения указателя, который я могу спокойно пройти через C ABI, я хочу известно рассмотрение этих различных методов, мое предпочтение будет метод 1.

Я не уверен, что все эти методы являются законными или безопасными.

Примечание: Объект всегда будет доступ функций в двоичном, из которого они созданы/уничтожены они возвращаются/принимать другие объекты, обрабатываемые ими типы самостоятельно или C-типа данных (до структур из СОД)

Хотя я нашел упоминание о таких вещах в сети, все они касаются людей, имеющих проблемы в результате преобразования в void, т. Е. A* a= static_cast<A*>(static_cast<void*>(c)) // c -> C*, что следует ожидать, поскольку это не исправляет vtable, и решениями является использование абстрактного базовый тип (этот wron't работает для меня, поскольку мне нужно пройти через C ABI), однако я также слышал упоминание о том, что виртуальные указатели больше обычных указателей, поэтому моя причина для рассмотрения методов 3 и 4, поскольку это было бы нормальный указатель на более крупный указатель и, таким образом, безопасный даже для типов с большими указателями.

Так что мой главный вопрос - это метод 1 работать с проблемой? Aslo я мог бы безопасно определить функцию шаблона по линиям template <typename T, typename U> void * void_cast(U v) { static_cast<void *>(static_cast<T>(v)); }, чтобы упростить код плагина. Наконец, если метод 1 является правильным, почему? и может ли использоваться какой-либо из методов?

ответ

1

Правило гласит, что вы можете конвертировать туда и обратно из указателя на объект на указатель базового класса, и из указателя на объект в void *. Но нет никаких гарантий, что все эти указатели имеют одинаковое значение (и даже такое же представление)!

Саид по-разному с примерами, где С является производным от:

C* c = new C; 
A* a = static_cast<A*>(c); // legal 
C* c1 = static_cast<C*>(a); // ok c1 == c guaranteed 

void *vc = static_cast<void *>(c); // legal 
C* c2 = static_cast<C*>(vc); // ok, c2 == c guaranteed 

void *va = static_cast<void *>(a); // legal, but va == vc is not guaranteed 

a2 = static_cast<A*>(vc); // legal, but a2 == a not guaranteed 
          // and dereferencing a2 is Undefined Behaviour 

Это означает, что если v построен как void *v = static_cast<void *>(c); а затем передается на ваш метод AfromCstatic_cast<A*>(v) может не указывать на действительный объект. И оба метода (1) и (2) - нет-op, потому что вы отбрасываете из void* указателю на obj и обратно, который требуется для получения исходного значения.

Для метода (4), но вы указали указатель на void на указатель на указатель, от указателя к указателю на указатель на указатель, а затем обратно в пустоту. Как 3.9.2 типы соединения [basic.compound] заявляет:

3 ... Указатели на макет-совместимые типы должны иметь такое же представление значения и требования к выравниванию ...

Как все указатели макет совместимых типов, вторая операция не должна изменять значение, и мы вернулись в no-op метода (1) и (2)

Метод (3) не должен компилироваться, поскольку вы принимаете адрес static_cast, и это не значение lvalue.

TL/DR: методы (1), (2) и (4) не имеют значения, означающие, что вы возвращаете входное значение без изменений, а метод (3) является незаконным, поскольку оператор & требует lvalue.

Единственный добросовестный способ преобразовать пустоту *, указывающую на объект C к чему-то, что может быть благополучно преобразовано в А *:

void * AfromC(void *v) { 
    C* c = static_cast<C*>(v); // v shall be static_cast<void>(ptr_to_C) 
    A* a = static_cast<A*>(c); 
    return static_cast<void *>(a); // or return a; with the implicit void * convertion 
} 

или как выражение одной линии

void * AfromC(void *v) { 
    return static_cast<A*>(static_cast<C*>(v)); 
} 
+0

Можете ли вы уточнить «но va == vc не гарантируется», учитывая, что я передал va в a как A * = static_cast (va), могу ли я затем передать это объекту типа C, то есть либо static_cast (a), либо reinterpret_cast (a), учитывая, что я знаю, что va был преобразован как в y наш ответ от объекта типа C, или это не будет более безопасным после литья через тип пустоты? – glenflet

+0

«разыменование a2 - неопределенное поведение», чтобы объяснить, почему вы так думаете? Из того, что я могу сказать, в общем представлении 'a2' и' c2' производятся тождественно из 'a' и' c', поэтому они должны быть одинаково UB - либо да, либо нет, а не наполовину. – dascandy

+0

@glenflet: Никакой гарантии для этого не может быть найден в стандарте. А в реализациях, использующих vtables для виртуальных функций, распространено, что адрес объекта является адресом vtable. В этом случае static_cast выполняет коррекцию смещения, но фактическое значение 'va' и' vc' отличается в этом случае. Просто попробуйте с отладчиком и базовыми и подклассами, имеющими виртуальные функции. –

0
return static_cast<void*>(static_cast<A*>(v)); // method 1 
return reinterpret_cast<void*>(static_cast<A*>(v)); // method 2 

Если void* v указывает на экземпляр типа C, то static_cast<A*>(v) неправильно.

//method 3 & 4 
C** c = static_cast<C**>(v); // Known to be C* type 
return static_cast<void*>(&static_cast<A*>(*c)); // method 3 
return static_cast<void*>(static_cast<A**>(c)); // method 4 

Если void* v указывает на экземпляр типа C, то static_cast<C**>(v) неправильно.


Будьте очень осторожны литья void* в правильный тип заостренным объекта. Я бы предпочел использовать static_cast, когда смогу, а не reinterpret_cast. Я также предпочитаю неявные приведения для доступа к подобъекту базы и для преобразования в void*. Уменьшенный шаблон меньше напряжения для глаз.

void* AfromC(void* v) { 
    C* c = static_cast<C*>(v); // Known to be C* type 
    A* a = c;     // point to base sub object 
    return a;     // implicit conversion to void* 
}