Как уменьшить код реализации множества классов-оболочек?

Я разрабатываю библиотеку с некоторыми классами, назовем их C1, C2 and ... Cn. Каждый из этих классов реализует некоторые интерфейсы, то есть I1, I2, ... Im. (n > m). Отношения между объектами в библиотеке сложны, и я должен предоставить некоторый API для пользователей моей библиотеки для доступа к этим объектам с помощью интеллектуальных указателей.

После некоторых обсуждений я пришел к выводу, что возвращать общие указатели пользователям библиотеки — не лучшая идея, потому что в этом случае я не могу быть уверен, что объект может быть удален именно в памяти моей библиотеки. Возврат слабых указателей имеет ту же проблему, потому что, если пользователь API .lock()s слабый указатель и сохранит где-то полученный общий указатель, я снова столкнусь с той же проблемой.

Последняя идея, которая у меня есть, состоит в том, чтобы предоставить какие-то обертки для слабых указателей. Класс-оболочка может быть примерно таким:

class Wrapper_C1 : public I1
{
   std::weak_ptr<C1> mC1;
public:
   Wrapper_C1() = delete;
   Wrapper_C1(const std::weak_ptr<C1> & c1) : mC1(c1)
   {
   }

   int method1_C1(int x)
   {
       if (auto sp = mC1.lock())
       {
           sp->method1_C1(x);
       }
       else
       {
            throw std::runtime_error("object C1 is not loaded in the lib.");
       }
   }

   void method2_C1(double y)
   {
       if (auto sp = mC1.lock())
       {
           sp->method2_C1(y);
       }
       else
       {
            throw std::runtime_error("object C1 is not loaded in the lib.");
       }
   }

   // The same for other methods
};

Как видите, все эти классы-оболочки имеют одну и ту же реализацию. Каков наилучший способ уменьшить код ВСЕХ этих классов-оболочек? Есть ли способ избежать повторения одинаковых кодов?


person Gupta    schedule 23.10.2019    source источник
comment
Вы имеете в виду «рефакторинг»?   -  person dandan78    schedule 23.10.2019
comment
@ dandan78 Нет, я отредактировал заголовок сообщения.   -  person Gupta    schedule 23.10.2019
comment
Метапрограммирование (имена интерфейсов могут быть статическими) или Bridge Design Pattern должны быть хорошими.   -  person seccpur    schedule 23.10.2019
comment
@seccpur Есть примеры?   -  person Gupta    schedule 23.10.2019
comment
@Gupta это может быть возможно с __declspec(propety .... это может позволить геттеру (проверка) и сеттеру (регистрация), все еще делая '.' возможен синтаксис - для этого потребуется как минимум clang или msvc   -  person darune    schedule 23.10.2019
comment
@darune Выглядит мило. У вас есть примеры того, как его использовать?   -  person Gupta    schedule 23.10.2019
comment
Что вы имеете в виду под Я не могу убедиться, что объект может быть выгружен именно в моей библиотеке? Вы выгружаете общую библиотеку?   -  person Maxim Egorushkin    schedule 23.10.2019
comment
что объект можно выгрузить именно в мою библиотеку. Что ты имел в виду? динамическая загрузка общей библиотеки (dlsym/LoadLibrary)? Затем вы можете предоставить оболочку, чтобы обеспечить время жизни объекта/библиотеки.   -  person Jarod42    schedule 23.10.2019
comment
@MaximEgorushkin Не буду выгружать общую библиотеку. Объекты C1, C2 и Cn являются частью графа в библиотеке. Пользователь API может захотеть выгрузить один объект графа, в этом случае некоторые зависимые объекты графа будут удалены из памяти моей библиотеки. Итак, если у пользователя есть общий указатель на них, я не могу гарантировать, что тогда у объектов не будет памяти.   -  person Gupta    schedule 23.10.2019
comment
@ Jarod42 Jarod42 Как я ответил на комментарий Максима, все объекты C1, C2 и Cn являются частью графа в моей библиотеке, который можно загружать или выгружать из/в файл. Я не имею в виду загрузку библиотеки здесь.   -  person Gupta    schedule 23.10.2019


Ответы (3)


Если вы отбросите наследование в оболочке, вы можете сделать что-то вроде следующего, чтобы разложить все оболочки на множители:

template <typename T>
class Wrapper
{
private:
   std::weak_ptr<T> m;
public:
   Wrapper() = delete;
   Wrapper(const std::weak_ptr<T> & w) : m(w) {}

   auto operator -> () /* const */
   {
       if (auto sp = m.lock())
       {
           return sp;
       }
       else
       {
            throw std::runtime_error("object is not loaded in the lib.");
       }
   }
};
person Jarod42    schedule 23.10.2019
comment
Я должен признать, что это даже лучше, чем у меня ;) - person bartop; 23.10.2019
comment
Вау, отлично выглядит. - person Gupta; 23.10.2019
comment
@firda: Старайтесь остерегаться Мерфи, а не Макиавелли :-). - person Jarod42; 23.10.2019

Лучшее, что вы можете сделать, не прибегая к макросам (которые здесь также не помогут, для полного решения вашей проблемы нам понадобится какое-то статическое отражение), это исправить эти повторения:

if (auto sp = mC1.lock())
{
    sp->method1_C1();
}
else
{
     throw std::Exception("object C1 is not loaded in the lib.");
}

То, что я вижу, вы можете легко уменьшить до функции шаблона, подобной этой:

template<class T, class R, class... Args>
R call_or_throw(const std::weak_ptr<T>& ptr, const std::string& error, R (T::*fun)(Args...), Args... args) {
    if (auto sp = ptr.lock()) 
    {
        return std::invoke(fun, *sp, args...);
    }
    else 
    {
        throw std::runtime_error(error.c_str());
    }
}

Чем вы можете использовать его так:

int method1_C1(int x)
{
    return call_or_throw(mC1, "object C1 is not loaded in the lib.", &C1::method1_C1, x);
}

void method2_C1(double y)
{
    return call_or_throw(mC1, "object C1 is not loaded in the lib.", &C1::method2_C1, y);
}

Из него даже можно сделать макрос.

person bartop    schedule 23.10.2019
comment
Выглядит хорошо, спасибо. Кажется, что реализация ВСЕХ методов неизбежна, не так ли? - person Gupta; 23.10.2019
comment
@Gupta да, невозможно автогенерировать методы по какой-то схеме. Как я уже сказал, вам нужно либо статическое отражение в cpp, либо генератор статического кода. - person bartop; 23.10.2019

Использование интеллектуальных указателей для узлов дерева/графа далеко не идеально. Деструкторы узлов дерева уничтожают интеллектуальные указатели на дочерние узлы, а те, в свою очередь, вызывают деструкторы дочерних узлов, что приводит к рекурсии, которая может переполнить стек, когда деревья глубокие или доступный размер стека мал.

Альтернативный дизайн — иметь класс дерева, который управляет временем жизни своих узлов и использует простые указатели, а-ля std::map. И есть правило, согласно которому удаление узла делает недействительными указатели и ссылки на удаленное поддерево.

Такая конструкция проста, надежна и наиболее эффективна во время выполнения.

person Maxim Egorushkin    schedule 23.10.2019
comment
Я создал на своей машине большое бинарное дерево с 24 уровнями и 1.67772e+07 узлами, каждый узел содержит два общих указателя. Это самое большое дерево, которое я могу создать на своем ноутбуке, потому что с 25 уровнем оно не может выделить память для общих указателей. При уничтожении a занимает некоторое время, но не переполняет стек. Причина понятна, потому что у меня не будет более 24 активационных записей на вершине стека для вызова вложенных деструкторов. - person Gupta; 23.10.2019
comment
@Gupta Со сбалансированным деревом вам это может сойти с рук. - person Maxim Egorushkin; 23.10.2019
comment
Но вы правы, если глубина моего дерева превысит 750 (для несбалансированного дерева), у меня будет переполнение стека при уничтожении. Хорошая точка. Спасибо ;) - person Gupta; 23.10.2019