Существует ли возможность получить доступ к методу класса-эксплуататора изнутри метода класса-работника?

Рейтинг: -1Ответов: 1Опубликовано: 20.05.2023

Имеется класс Автомобиль, он содержит в себе в качестве члена данных экземпляр класса Коробка_передач. Коробка передач проверяет свое состояние (температуру масла) и если температура масла слишком большая, ей необходимо сообщить хозяину, что один из его агрегатов сломан, чтобы Автомобиль зажег лампочку на панели приборов (в данном случае хозяин коробки передач это автомобиль). Другими словами, как из метода Коробки передач вызвать метод Автомобиля. В данном коде, я использую вымышленную функцию get_owner_pointer, вызов которой возвращает указатель на Автомобиль. Но как написать эту функцию я не знаю,- чтоб она возвращала указатель на класс-хозяина (Автомобиль), будучи вызванной внутри класса-работника (Коробка передач эксплуатируется Автомобилем в качестве работника). Я знаю только способ, когда принудительно внутрь Коробки передач передается указатель на Автомобиль (или делегат на метод Автомобиля). А вот можно ли не передавая заранее указатель или какую нибудь другую ниточку на класс-хозяина получить изнутри класса-работника доступ к методу класса-хозяина?

struct Autogearbox {
    void select_drive_mode(Enum_drive_mode  drive_mode ){
    // here gearbox checks its temperature, if overheat it must inform holder in someway
    // in this case the holder is car
    if ( is_overheat_gearbox() ) { get_owner_pointer()->service_required() }
    }  
}; 

 struct Car {
    void service_required() { // the code to lightON red lamp on driver front panel}
    Autogearbox gearbox{}; // here Car holds the gearbox as datamember
};

Ответы

▲ 1Принят

Хорошие варианты:

  • Сделать у Autogearbox геттер в духе bool IsBroken() const;, который Car сам бы звал время от времени.

  • Возвращать из каких-то методов Autogearbox результат - сломано или не сломано.

  • Передавать в какие-то методы Autogearbox колбек (std::function<void()> onBreak), и звать его при поломке. Еще варианты - отдавать указатель на интерфейс, от которого бы наследовался Car, и в котором была бы функция virtual void onAutogearboxBreak() = 0;.

  • Кидать исключение при поломке. Учитывайте, что исключения медленные, и это подходит, если поломки редкие.

Плохие варианты:

  • Передавать указатель на Car. Потому что тогда получается, то Autogearbox и Car могут использоваться только вместе, и становится не очень понятно, зачем иметь два класса, и почему не сложить все в один.

    В этом смысле выглядит лучше передавать колбек, или указатель на интерфейс, от которого наследовать Car.

  • Передавать колбек/указатель, и сохранять его в поле Autogearbox, вместо того, чтобы использовать сразу.

    Если Car переместят, то указатель на него протухнет. (Если там колбек вместо указателя, то внутри лямбды, которую вы будете туда передавать, скорее всего тоже окажется указатель на Car.)

    Этого можно избежать, сделав Car некопируемым и неперемещаемым, но это не лучший ход.

Хитроумные варианты:

  • Наследовать Car от Autogearbox. В последнем тогда была бы чисто виртуальная функция, которую тот бы звал при поломке.

    Вариации:

    • Тоже наследовать, но с CRTP. Немного сэкономите на вызове виртуальных функций.

    • Вытащить метод поломки в интерфейс, наследоваться от него виртуально и в Autogearbox и в Car, и в последнем переопределять метод. Тогда несколько компонентов Car смогут дергать один и тот же метод.

  • Если нужно несколько Autogearbox на один Car, то такие варианты:

    • Наследоваться от Autogearbox, но сделать его шаблоном, чтобы можно было давать ему имя: struct Car : Autogearbox<"gearbox_a">, Autogearbox<"gearbox_b"> (как передавать строки в шаблонные параметры).

    • Сделать Autogearbox полем Car, и хранить в нем смещение до Car. Смещение брать из offsetof(Car, my_gearbox). Использовать изнутри Autogearbox примерно так: (Car *)((char *)this - смещение). (Есть подозрение, что это UB, но на практике почти наверняка не сломается.)

      Тогда желательно сделать эти поля приватными, чтобы никто случайно не вынул их Car. Чтобы не привязываться к типу Car, можно сделать Autogearbox шаблоном и передавать Car как шаблонный аргумент.

    • То же самое, но сэкономить на хранении смещения, прокинув его через stateful metaprogramming:

запустить на gcc.godbolt.org

#include <cstddef>
#include <iostream>
#include <type_traits>

#if defined(__GNUC__) && !defined(__clang__)
#define NON_TEMPLATE_FRIEND(...) \
    _Pragma("GCC diagnostic push") \
    _Pragma("GCC diagnostic ignored \"-Wnon-template-friend\"") \
    __VA_ARGS__ \
    _Pragma("GCC diagnostic pop")
#else
#define NON_TEMPLATE_FRIEND(...) __VA_ARGS__
#endif

#define CAT(x, y) CAT_(x, y)
#define CAT_(x, y) x##y

// Macros to determine the type of the enclosing class.

namespace SelfType
{
    template <typename Key>
    struct Read
    {
        NON_TEMPLATE_FRIEND(
        friend constexpr auto _adl_SelfType(Read<Key>);
        )
    };

    template <typename Key, typename Value>
    struct Write
    {
        friend constexpr auto _adl_SelfType(Read<Key>) {return Value{};}
    };

    constexpr void _adl_SelfType() {} // Dummy ADL target.

    template <typename T>
    using Type = std::remove_pointer_t<decltype(_adl_SelfType(Read<T>{}))>;
}

// Creates a typedef named `self_`, pointing to the enclosing class.
// You can have several of those in a class, if the names are different.
#define SELF_TYPE(self_) \
    struct CAT(_adl_SelfTypeTag, self_) {}; \
    auto _adl_SelfTypeHelper(CAT(_adl_SelfTypeTag, self_)) \
    -> decltype(void(::SelfType::Write<CAT(_adl_SelfTypeTag, self_), decltype(this)>{})); \
    using self_ = ::SelfType::Type<CAT(_adl_SelfTypeTag, self_)>;

// Stateful metaprogramming to store the offset.

namespace OwnerOffset
{
    template <typename Key>
    struct Read
    {
        NON_TEMPLATE_FRIEND(
        friend constexpr auto _adl_OwnerOffset(Read<Key>);
        )
    };

    template <typename Key, std::ptrdiff_t Offset>
    struct Write
    {
        friend constexpr auto _adl_OwnerOffset(Read<Key>) {return Offset;}
    };

    constexpr void _adl_OwnerOffset() {} // Dummy ADL target.

    template <typename T>
    constexpr std::ptrdiff_t Value = _adl_OwnerOffset(Read<T>{});
}

template <typename Owner, typename Tag>
struct KnowsOwner
{
    [[nodiscard]] Owner &owner()
    {
        return *(Owner *)((char *)this - OwnerOffset::Value<Tag>);
    }
    [[nodiscard]] const Owner &owner() const
    {
        return const_cast<KnowsOwner *>(this)->owner();
    }
};

#define MAKE_VAR(name_, .../*type*/) \
    IMPL_MAKE_VAR(__COUNTER__, name_, __VA_ARGS__)

#define IMPL_MAKE_VAR(counter_, name_, .../*type*/) \
    /* Determine self type. */\
    SELF_TYPE(CAT(_self_,counter_))\
    /* Make a helper struct. */\
    struct CAT(_tag_,counter_) {};\
    /* Make our variable. */\
    __VA_ARGS__<CAT(_self_,counter_), CAT(_tag_,counter_)> name_{}; \
    /* Store the offset. */\
    static void CAT(_offset_writer_,counter_)() {(void)::OwnerOffset::Write<CAT(_tag_,counter_), offsetof(CAT(_self_,counter_), name_)>{};}\
    /* Force instantiate the previous function. */\
    static constexpr std::integral_constant<void (*)(), &CAT(_offset_writer_,counter_)> CAT(_offset_writer2_,counter_) {};

// ---

template <typename Owner, typename Tag>
struct A : KnowsOwner<Owner, Tag>
{
    int unused = 0;

    void foo()
    {
        std::cout << this->owner().x << '\n';
    }
};

struct B
{
    int x = 42;
    MAKE_VAR(f, A)
    MAKE_VAR(g, A)
};

int main()
{
    B b;
    b.f.foo(); // 42
    b.g.foo(); // 42
}