Nano Hash - криптовалюты, майнинг, программирование

Обертывание C++ в C: преобразование производных в базовые

Я оборачиваю простую иерархию наследования C++ в «объектно-ориентированный» C. Я пытаюсь выяснить, есть ли какие-либо ошибки в обработке указателей на объекты C++ как указатели на непрозрачные структуры C. В частности, при каких обстоятельствах преобразование производных данных в базовые вызовет проблемы?

Сами классы относительно сложны, но иерархия поверхностна и использует только одиночное наследование:

// A base class with lots of important shared functionality
class Base {
    public:
    virtual void someOperation();
    // More operations...

    private:
    // Data...
};

// One of several derived classes
class FirstDerived: public Base {
    public:
    virtual void someOperation();
    // More operations...

    private:
    // More data...
};

// More derived classes of Base..

Я планирую показать это C-клиентам через следующий довольно стандартный объектно-ориентированный C:

// An opaque pointers to the types
typedef struct base_t base_t;
typedef struct first_derived_t first_derived_t;

void base_some_operation(base_t* object) {
     Base* base = (Base*) object;
     base->someOperation();
}

first_derived_t* first_derived_create() {
     return (first_derived_t*) new FirstDerived();
}

void first_derived_destroy(first_derived_t* object) {
     FirstDerived* firstDerived = (FirstDerived*) object;
     delete firstDerived;
}

Клиенты C передают только указатели на базовые объекты C++ и могут манипулировать ими только через вызовы функций. Таким образом, клиент может, наконец, сделать что-то вроде:

first_derived_t* object = first_derived_create();
base_some_operation((base_t*) object); // Note the derived-to-base cast here
...

и виртуальный вызов FirstDerived::someOperation() преуспевает, как и ожидалось.

Эти классы не являются стандартным макетом но не используйте множественное или виртуальное наследование. Это гарантированно работает?

Обратите внимание, что я контролирую весь код (C++ и C-оболочку), если это имеет значение.

06.02.2012

  • Просто используйте void* везде как непрозрачный тип. 07.02.2012

Ответы:


1
// An opaque pointers to the types
typedef struct base_t base_t;
typedef struct first_derived_t first_derived_t;

// **********************//
// inside C++ stub only. //
// **********************//

// Ensures you always cast to Base* first, then to void*,
// then to stub type pointer.  This enforces that you'll
// get consistent a address in presence of inheritance.
template<typename T>
T * get_stub_pointer ( Base * object )
{
     return reinterpret_cast<T*>(static_cast<void*>(object));
}

// Recover (intermediate) Base* pointer from stub type.
Base * get_base_pointer ( void * object )
{
     return reinterpret_cast<Base*>(object);
}

// Get derived type pointer validating that it's actually
// the right type.  Returs null pointer if the type is
// invalid.  This ensures you can detect invalid use of
// the stub functions.
template<typename T>
T * get_derived_pointer ( void * object )
{
    return dynamic_cast<T*>(get_base_pointer(object));
}

// ***********************************//
// public C exports (stub interface). //
// ***********************************//

void base_some_operation(base_t* object)
{
     Base* base = get_base_pointer(object);
     base->someOperation();
}

first_derived_t* first_derived_create()
{
     return get_stub_pointer<first_derived_t>(new FirstDerived());
}

void first_derived_destroy(first_derived_t* object)
{
     FirstDerived * derived = get_derived_pointer<FirstDerived>(object);
     assert(derived != 0);

     delete firstDerived;
}

Это означает, что вы всегда можете выполнить приведение, подобное следующему.

first_derived_t* object = first_derived_create();
base_some_operation((base_t*) object);

Это безопасно, потому что указатель base_t* будет приведен к void*, а затем к Base*. Это на один шаг меньше, чем то, что было раньше. Обратите внимание на порядок:

  1. FirstDerived*
  2. Base* (через неявное static_cast<Base*>)
  3. void* (через static_cast<void*>)
  4. first_derived_t* (через reinterpret_cast<first_derived_t*>)
  5. base_t* (через (base_t*), который представляет собой reinterpret_cast<base_t*> в стиле C++)
  6. void* (через неявное static_cast<void*>)
  7. Base* (через reinterpret_cast<Base*>)

Для вызовов, обертывающих метод FirstDerived, вы получаете дополнительное приведение:

  1. FirstDerived* (через dynamic_cast<FirstDerived*>)
07.02.2012
  • Хорошо, это кажется и безопасным, и чистым, даже перед лицом будущих программистов обслуживания, которые могут не понять всю сквозную ситуацию. Спасибо! 07.02.2012

  • 2

    Вы, безусловно, можете создать интерфейс C для некоторого кода C++. Все, что вам нужно, это extern "C", и я рекомендую void * в качестве непрозрачного типа данных:

    // library.h, for C clients
    
    typedef void * Handle;
    
    extern "C" Handle create_foo();
    extern "C" void destroy_foo(Handle);
    
    extern "C" int magic_foo(Handle, char const *);
    

    Затем реализуйте его на С++:

    #include "library.h"
    #include "foo.hpp"
    
    Handle create_foo()
    {
        Foo * p = nullptr;
    
        try { p = new Foo; }
        catch (...) { p = nullptr; }
    
        return p
    }
    
    void destroy_foo(Handle p)
    {
        delete static_cast<Foo*>(p);
    }
    
    int magic_foo(Handle p, char const * s)
    {
        Foo * const f = static_cast<Foo*>(p);
    
        try
        {
            f->prepare();
            return f->count_utf8_chars(s);
        }
        catch (...)
        {
            return -1;
            errno = E_FOO_BAR;
        }
    }
    

    Помните, что нельзя допускать, чтобы какие-либо исключения распространялись через вызывающую функцию C!

    06.02.2012
  • Я делал это раньше, и использование void* действительно проблематично из соображений безопасности типов на стороне клиента (в качестве этого аргумента легко передать что угодно). Рассмотрим API Windows, где файлы и объекты мьютекса идентифицируются по типу HANDLE, что означает, что вы можете передать файл ReleaseMutex(). Вы можете использовать неполную структуру для каждого отдельного типа, который должен отображаться на стороне клиента. Например, struct foo_t; typedef struct foo_t* foo_t;. Затем используйте foo_t create_foo(); и void destroy_foo(foo_t);. 07.02.2012
  • Он добавляет несколько неприятных преобразований внутри реализации заглушки C++-to-C, но, по крайней мере, вы получаете типобезопасный интерфейс на стороне клиента. 07.02.2012
  • @Kerrek SB: Спасибо за ответ, но это проще, чем мой случай. Меня беспокоит приведение от FirstDerived* к Base* через приведение в стиле C. Например, если бы речь шла о множественном наследовании, у нас были бы проблемы, не так ли? Я ищу другие ошибки... 07.02.2012
  • @ Адриан, я думаю, вы, возможно, захотите обновить свой вопрос, чтобы сделать эту проблему о преобразованиях производных в базовые более явной. Этот текущий ответ не предполагает никакого наследования. Я думаю, что нет действительно безопасного способа разрешить любые преобразования в коде C; вам, возможно, придется реализовать множество функций, таких как Base * getBaseFromDerived1(Derived1 *) и Base * getBaseFromDerived2(Derived2 *) и так далее. 07.02.2012
  • @Adrian: Очень просто: вы всегда должны возвращать то же самое, что и изначально, к void *. На самом деле не имеет значения, связаны ли классы C++ и как; вы должны просто вернуть исходный шрифт прямо на нос. 07.02.2012
  • Действительно, вам может понадобиться static_cast<void*>(static_cast<Base*>(derived)), если это необходимо, чтобы убедиться, что точный адрес получен. 07.02.2012
  • @AndréCaron, да, мы можем использовать эти приведения в коде C ++, но вопрос касается приведения в коде C. Насколько я понимаю, мы не сможем безопасно разрешить приведение типов в коде C. 07.02.2012
  • @AaronMcDaid: да, можешь. Cast всегда приводит все указатели к Base*. В заглушках для функций производного класса используйте static_cast обратно к Derived* перед вызовом реальной функции. Вы даже можете использовать dynamic_cast, чтобы убедиться, что клиент не сделал глупости. 07.02.2012
  • @AndréCaron, может быть, я констатирую очевидное, но в самом коде C явно не может быть static_cast. Итак, мы не задаемся вопросом, как написать код C++, мы пытаемся придумать хороший интерфейс, чтобы сделать его доступным для кода C. Проблема заключается в том, что в коде C могут быть случаи, когда есть две переменные двух разных типов "дескрипторов" (derived1_t и base_t), которые ссылаются на один и тот же объект. Мы должны быть очень осторожны, если в коде C есть какие-либо приведения типов между этими двумя типами дескрипторов. 07.02.2012
  • @AaronMcDaid: перечитайте мой последний комментарий. Выполнение того, что я предложил, позволяет вам писать приведения в коде C, как в примере в вашем вопросе. Поскольку вы всегда выполняете приведение к Base* перед приведением к фиктивному типу указателя C, приведение к базовым классам безопасно на стороне C. 07.02.2012
  • @AndréCaron, я понимаю, что ты имеешь в виду. Ваша мысль правильная. Извинения. Я думаю, мне не нравится идея, что Derived* означает что-то другое в коде C, чем это означало бы в коде C++. Но это всего лишь стиль. Меня беспокоит то, что вы можете скомпилировать код C с помощью компилятора C++, и внезапно приведение типов (Base*) derived1_pointer сломается. 07.02.2012
  • @AaronMcDaid: я добавил свою идею в качестве ответа. Вы можете проверить это на наличие возможных недостатков. 07.02.2012

  • 3

    Это подход, который я использовал в прошлом (возможно, как следует из комментария Аарона). Обратите внимание, что в C и C++ используются одинаковые имена типов. Все приведения выполняются на C++; это, естественно, представляет собой хорошую инкапсуляцию независимо от вопросов законности. [Очевидно, что вам также нужны методы delete.] Обратите внимание, что для вызова someOperation() с Derived* требуется явное восходящее преобразование к Base*. Если Derived не предоставляет никаких новых методов, таких как someOtherOperation, вам не нужно предоставлять Derived* клиентам и избегать приведения типов на стороне клиента.

    Заголовочный файл: "BaseDerived.H"

    #ifdef __cplusplus
    extern "C"
    {
    #endif
        typedef struct Base Base;
        typedef struct Derived Derived;
    
        Derived* createDerived();
        Base* createBase();
        Base* upcastToBase(Derived* derived);
        Derived* tryDownCasttoDerived(Base* base);
        void someOperation(Base* base);
    void someOtherOperation(Derived* derived);
    #ifdef __cplusplus
    }
    #endif
    

    Реализация: "BaseDerived.CPP"

    #include "BaseDerived.H"
    struct Base 
    {
        virtual void someOperation()
        {
            std::cout << "Base" << std::endl;
        }
    };
    struct Derived : public Base
    {
    public:
        virtual void someOperation()
        {
            std::cout << "Derived" << std::endl;
        }
    private:
    };
    
    Derived* createDerived()
    {
        return new Derived;
    }
    
    Base* createBase()
    {
        return new Base;
    }
    
    Base* upcastToBase(Derived* derived)
    {
        return derived;
    }
    
    Derived* tryDownCasttoDerived(Base* base)
    {
        return dynamic_cast<Derived*>(base);
    }
    
    void someOperation(Base* base)
    {
        base->someOperation();
    }
    
    void someOperation(Derived* derived)
    {
        derived->someOperation();
    }
    
    07.02.2012

    4

    Я думаю, что эти две строки являются сутью вопроса:

    first_derived_t* object = first_derived_create();
    base_some_operation((base_t*) object); // Note the derived-to-base cast here
    ...
    

    Нет действительно безопасного способа разрешить это в коде C. В C такое приведение никогда не изменяет необработанное целочисленное значение указателя, но иногда это происходит при приведении C++, и поэтому вам нужен проект, который никогда не имеет приведения в коде C.

    Вот одно (слишком сложное?) решение. Во-первых, определитесь с политикой, согласно которой код C всегда будет строго обрабатывать значение, которое фактически является Base* - это несколько произвольная политика для обеспечения согласованности. Это означает, что код C++ иногда должен будет использовать dynamic_cast, мы вернемся к этому позже.

    (Вы можете заставить дизайн работать правильно с кодом C, просто используя приведения типов, как было упомянуто другими. Но я бы беспокоился, что компилятор допустит все виды сумасшедших приведений, таких как (Derived1*) derived2_ptr или даже приведения к типам в другая иерархия классов. Моя цель здесь — обеспечить правильное объектно-ориентированное отношение is-a в коде C.)

    Тогда классы дескрипторов C могут быть чем-то вроде

    struct base_t_ptr {
        void * this_; // holds the Base pointer
    };
    typedef struct {
        struct base_t_ptr get_base;
    } derived_t_ptr;
    

    Это должно упростить использование чего-то вроде приведения типов лаконичным и безопасным способом: обратите внимание, как мы передаем object.get_base в этом коде:

    first_derived_t_ptr object = first_derived_create();
    base_some_operation(object.get_base);
    

    где объявление base_some_operation

    extern "C" base_some_operation(struct base_t_ptr);
    

    Это будет довольно безопасным типом, так как вы не сможете передать производный1_t_ptr в эту функцию, не пройдя через элемент данных .get_base. Это также поможет вашему C-коду немного узнать о типах и допустимых преобразованиях — вы не хотите случайно преобразовать Derived1 в Derived2.

    Затем при реализации невиртуальных методов, определенных только в производном классе, вам понадобится что-то вроде:

    extern "C" void derived1_nonvirtual_operation(struct derived1_t_ptr); // The C-style interface. Type safe.
    
    void derived1_nonvirtual_operation(struct derived1_t_ptr d) {
        // we *know* this refers to a Derived1 type, so we can trust these casts:
        Base * bp = reinterpret_cast<Base*>(d.get_base.this_);
        Derived1 *this_ = dynamic_cast<Derived1*>;
        this_ -> some_operation();
    }
    
    07.02.2012
    Новые материалы

    Кластеризация: более глубокий взгляд
    Кластеризация — это метод обучения без учителя, в котором мы пытаемся найти группы в наборе данных на основе некоторых известных или неизвестных свойств, которые могут существовать. Независимо от..

    Как написать эффективное резюме
    Предложения по дизайну и макету, чтобы представить себя профессионально Вам не позвонили на собеседование после того, как вы несколько раз подали заявку на работу своей мечты? У вас может..

    Частный метод Python: улучшение инкапсуляции и безопасности
    Введение Python — универсальный и мощный язык программирования, известный своей простотой и удобством использования. Одной из ключевых особенностей, отличающих Python от других языков, является..

    Как я автоматизирую тестирование с помощью Jest
    Шутка для победы, когда дело касается автоматизации тестирования Одной очень важной частью разработки программного обеспечения является автоматизация тестирования, поскольку она создает..

    Работа с векторными символическими архитектурами, часть 4 (искусственный интеллект)
    Hyperseed: неконтролируемое обучение с векторными символическими архитектурами (arXiv) Автор: Евгений Осипов , Сачин Кахавала , Диланта Хапутантри , Тимал Кемпития , Дасвин Де Сильва ,..

    Понимание расстояния Вассерштейна: мощная метрика в машинном обучении
    В обширной области машинного обучения часто возникает необходимость сравнивать и измерять различия между распределениями вероятностей. Традиционные метрики расстояния, такие как евклидово..

    Обеспечение масштабируемости LLM: облачный анализ с помощью AWS Fargate и Copilot
    В динамичной области искусственного интеллекта все большее распространение получают модели больших языков (LLM). Они жизненно важны для различных приложений, таких как интеллектуальные..