Вызов защищенной виртуальной функции другого связанного объекта (для прокси)

Итак, задача: у нас есть сторонняя библиотека, есть класс (назовите его 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();
}

1

Решение

Вылетает именно на этой строке:

    (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 тоже.

Об основной идее: здесь мы пытаемся вызвать защищенную функцию разные объект. Ссылаясь на этот список, по сути то же самое сказано Вот

Члены класса, объявленные как защищенные, могут использовать только следующее:

  1. Функции-члены класса, который первоначально объявил эти члены.
  2. Друзья класса, который первоначально объявил этих участников.
  3. Классы, полученные с открытым или защищенным доступом от класса, который первоначально объявил эти члены.
  4. Прямые частные классы, которые также имеют частный доступ к защищенным членам.
  1. не тот случай
  2. друзья не объявлены
  3. пытаясь вызвать метод на другом объекте, не this
  4. не тот случай

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

1

Другие решения

Проблема в том, что вы многократно наследуете от обоих 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,

0