如何减少大量包装类的实现代码?

6
我正在开发一个库,其中包含一些类,我们称之为 C1、C2 和 … Cn。每个类都实现了一些接口,例如 I1、I2、… Im (n>m)。库中对象之间的关系很复杂,我必须为我的库用户提供一些 API,以使用智能指针访问这些对象。
经过一些讨论,我发现将共享指针返回给库用户并不是一个好主意,因为在这种情况下,我无法确保对象可以精确地从我的库内存中删除。返回弱指针也有同样的问题,因为如果 API 的用户 .lock() 弱指针并将所得到的共享指针放在某个地方,我会再次面临同样的问题。
最终的想法是公开某些弱指针的包装器。包装器类可以像这样:
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
};

如您所见,所有这些包装器类都共享相同的实现。 减少所有这些包装器类代码的最佳方式是什么? 有没有避免重复相似代码的方法?


你是指“重构”吗? - dandan78
@dandan78 不是,我修改了帖子标题。 - TonySalimi
元编程(接口名称可以是静态的)或桥接设计模式应该很好。 - seccpur
@darune 看起来不错。你有如何使用它的示例吗? - TonySalimi
@MaximEgorushkin 我不会卸载共享库。C1、C2和Cn对象是库中图形的一部分。API用户可能想要卸载图形中的一个对象,在这种情况下,图形的某些依赖对象将从我的库内存中删除。因此,如果用户在它们上面有一个共享指针,我不能确保这些对象已经被清除出内存。 - TonySalimi
显示剩余5条评论
3个回答

5
如果在包装器中取消继承,您可以执行以下操作来因式分解所有包装器:
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.");
       }
   }
};

1
我必须承认,这比我的还要好 ;) - bartop
哇,看起来很棒。 - TonySalimi
shared_ptr<int> stolen; void func(Wrapper<int> ptr) { stolen = ptr.operator->(); } - firda
1
@firda:尽量防范墨菲,而不是马基雅维利 :-). - Jarod42

4

如果不使用宏(在此也没有帮助,为了完全解决问题,我们需要某种静态反射),你能做的最好就是修复这些重复:

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);
}

你甚至可以用它制作宏。

看起来不错,谢谢。似乎所有方法的实现都是不可避免的,对吗? - TonySalimi
@Gupta 是的,根据某种方案自动生成方法是不可能的。正如我所说,你需要在cpp中使用静态反射或静态代码生成器。 - bartop

2
使用智能指针作为树/图节点并不是最理想的选择。当树很深或可用堆栈大小较小时,树节点析构函数会销毁子节点的智能指针,这些智能指针又会调用子节点析构函数,导致递归,可能会导致堆栈溢出。
一种替代方案是使用一个管理其节点生命周期并使用裸指针的树类,类似于std::map。并规定删除节点会使指向已删除子树的指针和引用失效。
这样的设计简单、健壮且运行时效率最高。

我在我的计算机上创建了一个24层,具有16777200个节点的大二叉树,每个节点都包含两个shared_ptr。这是我在笔记本电脑上可以创建的最大树,因为在第25层时无法为shared_ptr分配内存。删除过程需要一些时间,但不会导致堆栈溢出。原因很明确,因为对于调用嵌套析构函数而言,在堆栈顶部不会有超过24个激活记录。 - TonySalimi
1
@Gupta 如果使用平衡树,你可能可以解决这个问题。 - Maxim Egorushkin
但是你说得对,如果我的树深度超过750(对于不平衡的树),我在销毁时会遇到堆栈溢出的问题。好观点。谢谢 ;) - TonySalimi

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接