Итак, задача: у нас есть сторонняя библиотека, есть класс (назовите его Base). В библиотеке есть скрытая реализация, называемая Impl.
Мне нужно написать прокси. К сожалению, Base имеет защищенную виртуальную функцию fn.
Таким образом, вопрос в том, насколько приведенный ниже код является правильным с точки зрения C ++? В настоящее время он отлично работает в Visual Studio и не работает в clang / gcc на Mac (но компилируется без каких-либо предупреждений). Я вполне понимаю механизмы, которые там происходят, поэтому, если удалить класс Problem, все работает на обеих платформах. Я хотел бы знать, должен ли я сообщать об ошибке в Clang или о неопределенном / неуказанном поведении стандарта C ++.
Ожидаемый результат кода — нормально вызывать Impl :: fn ()
class Base
{
protected:
virtual void fn(){}
};
class Impl : public Base
{
public:
Impl() : mZ(54){}
protected:
virtual void fn()
{
int a = 10; ++a;
}
int mZ;
};
class Problem
{
public:
virtual ~Problem(){}
int mA;
};
class Proxy : public Problem, public Base
{
public:
virtual void fn()
{
Base * impl = new Impl;
typedef void (Base::*fn_t)();
fn_t f = static_cast<fn_t>(&Proxy::fn);
(impl->*f)();
delete impl;
}
};
int main()
{
Proxy p;
p.fn();
}
Вылетает именно на этой строке:
(impl->*f)();
Попытка получить доступ к памяти за выделенным блоком. Обычно это намек на то, что никто не настроил this
правильно, и действительно, обмен порядком наследования исправляет проблему, подтверждая эту теорию.
Base * impl = new Impl;
typedef void (Base::*fn_t)();
fn_t f = static_cast<fn_t>(&Proxy::fn);
(impl->*f)();
Таким образом, проблема в том, где fn_t указывает (конечно, не запись vtable Base :: fn здесь).
Теперь мы видим проблему по-настоящему. Вы пытаетесь вызвать защищенную функцию другого объекта, пытаясь использовать &Base :: fn для этого невозможен, попытка использовать указатель на Proxy :: fn — это фактически другая функция с другим индексом vtable, которого нет в Base.
Теперь это работает только потому, что MSVC использует другую структуру памяти, где Proxy :: fn и Base :: fn по совпадению имеют одинаковый индекс vtable. Попробуйте поменять порядок наследования в сборке MSVC, и это может привести к сбою. Или попробуйте добавить другую функцию или член где-нибудь, рано или поздно будут Наверное, сбой с MSVC тоже.
Об основной идее: здесь мы пытаемся вызвать защищенную функцию разные объект. Ссылаясь на этот список, по сути то же самое сказано Вот
Члены класса, объявленные как защищенные, могут использовать только следующее:
- Функции-члены класса, который первоначально объявил эти члены.
- Друзья класса, который первоначально объявил этих участников.
- Классы, полученные с открытым или защищенным доступом от класса, который первоначально объявил эти члены.
- Прямые частные классы, которые также имеют частный доступ к защищенным членам.
this
Поэтому я не думаю, что это законно, что приводит к неопределенному поведению, безразличному к любому умному кастингу и т. Д.
Проблема в том, что вы многократно наследуете от обоих Base
а также Problem
, Расположение классов ABI не определено стандартом, и реализации могут выбирать, как они размещают объекты, поэтому вы видите разные результаты на разных компиляторах.
В частности, причиной сбоя является то, что ваш производный класс заканчивается двумя v-таблицами: одна для Base
а также Problem
,
Я случай G ++, так как вы наследуете public Problem, public Base
макет класса имеет V-таблицу для Problem
в «традиционном» месте, и V-стол для Base
позже в макете класса.
Если вы хотите увидеть это в действии, добавьте это в свой main
…
int main()
{
Proxy p;
Base *base = &p;
Problem *problem = &p;
std::cout << "Proxy: " << &p << ", Problem: " << problem << ", Base: " << base << '\n';
}
Вы увидите нечто похожее на это …
Proxy: 0x7fff5993e9b0, Problem: 0x7fff5993e9b0, Base: 0x7fff5993e9c0
Теперь вы делаете что-то «злое» здесь:
typedef void (Base::*fn_t)();
fn_t f = static_cast<fn_t>(&Proxy::fn);
(impl->*f)();
потому что вы берете указатель на функцию-член для Proxy
и применяя его к Impl
объект. Да, они оба наследуют от Base
, но вы дали ему указатель на функцию-член для класса Proxy
и когда он просматривает эту таблицу, они находятся в разных местах.
Вы действительно хотите получить указатель на функцию-член для Base
но так как вы делаете это из контекста Proxy
вы можете получить доступ только к Proxy
функция-член. Теперь должно быть очевидно, что это нежелательно из-за множественного наследования.
Тем не менее, вы можете легко получить то, что я думаю, что вы хотите с небольшим классом помощника …
virtual void fn()
{
typedef void (Base::*fn_t)();
struct Helper : Base {
static fn_t get_fn() { return &Helper::fn; }
};
Base * impl = new Impl;
fn_t f = Helper::get_fn();
(impl->*f)();
delete impl;
}
Так как Helper
наследуется от Base
он имеет доступ к защищенному члену, и вы можете получить к нему доступ вне контекста множественного наследования Proxy
,