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

Итак задача: у нас есть сторонняя библиотека, есть класс (назовем его Base). Существует скрытая реализация, предоставляемая библиотекой, называемой Impl. Мне нужно написать прокси. К сожалению, в Base есть защищенная виртуальная функция fn.

Итак, вопрос в том, насколько приведенный ниже код верен с точки зрения C++? В настоящее время он отлично работает в Visual Studio и не работает в clang/gcc на Mac (но компилируется без каких-либо предупреждений). Я прекрасно понимаю механизмы, которые там происходят, так что если убрать класс Problem, то все работает на обеих платформах. Я хотел бы знать, должен ли я сообщить об ошибке в clang или о неопределенном/неуказанном поведении стандарта С++.

Ожидаемый результат кода - обычный вызов 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();
}

person dev_null    schedule 08.10.2014    source источник


Ответы (2)


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

    (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, который не существуют в Базе.

Теперь это работает только потому, что MSVC использует другой макет памяти, где по совпадению Proxy::fn и Base::fn имеют один и тот же индекс vtable. Попробуйте поменять порядок наследования в сборке MSVC, и это может привести к сбою. Или попробуйте добавить куда-нибудь другую функцию или член, я думаю, рано или поздно он вылетит из-за MSVC.

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

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

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

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

person dom0    schedule 08.10.2014
comment
Спасибо, я знаю, как это работает и почему происходит сбой. И он не будет падать с MSVC. Я хочу выяснить, является ли это правильным (хотя и немного странным) кодом С++, который должен работать везде. Кстати, я рад, что вы понимаете, где тонкость и как она должна работать :) - person dev_null; 08.10.2014
comment
Я не думаю, что это четко определенное поведение, поскольку вы фактически вызываете защищенную функцию другого объекта, что недопустимо. - person dom0; 08.10.2014
comment
@dev_null что значит правильно? Вы намеренно обходите стандарт, чтобы сделать что-то, что указано как незаконное. Конечно, он не должен работать везде, в этом смысл стандарта: определить, что должно работать. тл; др : Нет. - person Félix Cantournet; 08.10.2014
comment
Что касается визуальной студии после вашего следующего редактирования: Нет, она работает независимо от порядка классов и количества виртуальных функций. ИМХО правильно. VS использует другую модель для представления pmfs (указателей на функции-члены). Что касается вызова защищенной функции: я могу вызвать ее для другого объекта, если объект того же типа. то, что вызывает Proxy::fn() из mMyOtherProxyObject, полностью в порядке. Здесь важно приведение указателей (из Proxy в Base и из Base в Impl). Я даже могу изменить вопрос: что, если здесь все «публично». Должен ли работать пример? - person dev_null; 08.10.2014
comment
Что ж, если бы все было действительно общедоступным, вы могли бы вызывать функцию напрямую через базовый указатель... - person dom0; 08.10.2014

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

Вы действительно просто хотите получить указатель функции-члена для 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.

person Jody Hagins    schedule 08.10.2014
comment
Спасибо, поскольку прокси на моей стороне, я могу выбрать порядок наследования. а также я понимаю проблему с множественным наследованием. опять же главный вопрос в том, в какой степени код правильный (независимо от компилятора и порядка наследования). То есть я вправе ожидать, что это будет работать, например, на компиляторе Intel или на компиляторе C++ 10 лет спустя на другом процессоре/платформе. - person dev_null; 08.10.2014
comment
Вы не можете делать никаких предположений об этом, за исключением того, что он, вероятно, сломается с разными компиляторами, если вы будете делать то, что делаете. Вспомогательный обходной путь должен предоставить то, что вы хотите, с любым компилятором. - person Jody Hagins; 08.10.2014