【问题标题】:Is partial class template specialization the answer to this design problem?部分类模板专业化是这个设计问题的答案吗?
【发布时间】:2010-09-26 06:11:47
【问题描述】:

假设您有一个班级,其工作是连接到远程服务器。我想抽象这个类来提供两个版本,一个通过UDP连接,另一个通过TCP连接。我想构建尽可能精简的运行时代码,而不是使用多态性,我正在考虑模板。这是我的设想,但我不确定这是不是最好的方法:

class udp {};
class tcp {};

template<class T,typename X>
class service
{
private:
  // Make this private so this non specialized version can't be used
   service();
};

template<typename X>
class service<udp, X>
{
private:
   udp _udp;
   X _x;
};

template<typename X>
class service<tcp, X>
{
private:
   tcp _tcp;
   X _x;
};

所以最终的好处是 T 的通用性仍然可用,但是设置 UDP 或 TCP 连接所需的非常不同的代码已经被专门化了。我想你可以把它放在一个类中,或者提供另一个类,它遵循一些纯虚拟接口来设置网络连接,比如 IConnectionManager。

但这确实留下了泛型 T 的代码现在必须在两个专用版本中编写和维护的问题,它们最终是相同的。如何最好地解决这个问题?我有一种感觉,我做错了。

【问题讨论】:

    标签: c++ oop templates specialization


    【解决方案1】:

    最好使用传输协议的策略来完成:

    template<typename Transport>
    class service : Transport {
    public:
        typedef Transport transport_type;
    
        // common code
        void do_something() { 
            this->send(....);
        }
    };
    
    class tcp {
    public:
        void send(....) {
    
        }
    };
    
    class udp {
    public:
        void send(....) {
    
        }
    };
    
    typedef service<tcp> service_tcp;
    typedef service<udp> service_udp;
    

    请注意,这也是多态的。它被称为编译时多态性。将策略放入基类将受益于 Empty-Base-Class-Optimization。也就是说,您的基类不需要占用任何空间。将策略设置为成员有另一个缺点,即您总是必须将事情委派给该成员,这可能会随着时间的推移而变得烦人。本书Modern C++ Design 深入描述了这种模式。

    理想情况下,传输协议不需要知道任何关于它上面的协议的信息。但是如果由于某种原因你必须得到一些关于它的信息,你可以使用 crtp 模式 wiki:

    template<template<typename Service> class Transport>
    class service : Transport<service> {
    
        // since we derive privately, make the transport layer a friend of us, 
        // so that it can cast its this pointer down to us. 
        friend class Transport<service>;
    
    public:
        typedef Transport<service> transport_type;
    
        // common code
        void do_something() { 
            this->send(....);
        }
    };
    
    template<typename Service>
    class tcp {
    public:
        void send(....) {
    
        }
    };
    
    template<typename Service>
    class udp {
    public:
        void send(....) {
    
        }
    };
    
    typedef service<tcp> service_tcp;
    typedef service<udp> service_udp;
    

    您不必将模板放入标题中。如果您显式实例化它们,您将获得更快的编译时间,因为需要包含的代码更少。将其放入 service.cpp:

    template class service<tcp>;
    template class service<udp>;
    

    现在,使用服务的代码不需要知道服务的模板代码,因为该代码已经生成到 service.cpp 的目标文件中。

    【讨论】:

    • 谢谢,typedef Transport transport_type 的用途是什么;在第一个例子中?
    • applepie 很好,typedef 模板参数总是好的,所以你可以从类的范围之外访问它们。例如,您也有 std::vector::value_type, std::vector::size_type。您还应该添加一个 typedef Service service_type;到 udp 和 tcp 类。
    • 另外,在手头的示例中,假设您在派生类中也有一个发送函数。调用 this->send(....);将无限递归。调用 Transport::send(....);是罗嗦的。你可以做 transport_type::send(...);那么
    【解决方案2】:

    我会使用奇怪的重复模板模式,也就是五点手掌爆炸 Alexandrescu 技术:

    template <typename Underlying>
    class Transmit
    {
    public:
       void send(...)
       {
          _U.send(...)
       };
    
    private:
        Underlying _U;
    };
    
    class Tcp
    {
    public:
       void send(...) {};
    };
    
    class Udp
    {
    public:
       void send(...) {};
    };
    

    可能会有更多的模板参数和子类,但你明白了,你也可以使用静态方法。

    顺便说一下,模板代码通常更高效,但也更大。

    【讨论】:

    • 有趣,这会将实际的协议特定代码重新定位到底层,并且基本上否定了它看起来的专业化需求。
    • 您的解决方案与 CRTP 有什么关系? Curiously Recurring Template Pattern 表示一个类型继承一个模板类并使用它自己作为模板参数的情况: template class Base {...};派生类:公共基础 {...};
    • 本例中没有 CRTP,因为它非常简单,但完整的版本可能是 CRTP。如果您愿意,我们可以使用术语基于策略的设计。
    • 但是我完全同意你的其他回答。 udp 或 tcp 特定的代码应封装在专用于传输的类中,以便服务不与特定协议耦合。
    • 我认为“好的”设计会比仅仅封装 udp 和 tcp 走得更远。
    【解决方案3】:

    模板不是必需的(尽管可能的解决方案)。这只是通过模板而不是通过构造函数的依赖注入。我个人会通过构造函数来做到这一点。但是通过模板执行此操作会为您提供更便宜的方法调用的可疑好处(它不需要是虚拟的)。但也允许更容易的编译器优化。

    udp 和 tcp 对象必须仍然支持相同的接口
    如果你通过继承来实现,它们都必须实现一个公共接口(虚拟基类),它是通过模板完成的,这不是必需的,但编译器会检查它们是否支持服务对象所需的相同方法调用。

    正如在原始问题中所问的那样,我认为部分模板专业化没有明确的需要(或好处)(在所描述的情况下)。

    模板方法

    class udp {/*Interface Plop*/static void plop(Message&);};
    class tcp {/*Interface Plop*/static void plop(Message&);};
    template<typename T>
    class Service
    {
        public:
            void doPlop(Message& m) { T::plop(m);}
        // Do not actually need to store an object if you make the methods static.
        // Alternatively:
        public:
            void doPlop(Message& m) { protocol.plop(m);}
        private:
            T protocol;
    };
    

    多态版本

    class Plop{virtual void plop(Message&) = 0;} // Destruct or omitted for brevity
    class upd:public Plop {/*Interface Plop*/void plop(Message&);};
    class tcp:public Plop {/*Interface Plop*/void plop(Message&);};
    class Service
    {
        public:
            Service(Plop& p):protocol(p)  {};
            void doPlop(Message& m) { protocol.plop(m);}
        private:
            Plop& protocol;
    };
    

    【讨论】:

    • 谢谢,确实有道理。我同意在这种情况下完全不需要专业化。关于好处是可疑的,我知道这是一个激烈的争论,消除 vtable 查找是否比其他优化更有价值,但我认为为我不使用的语言功能付费没有任何好处。
    • 你总是要为它买单(计算机之神要求它)。问题是你在哪里支付。您可以消除对 vtable 的需求和通过虚拟指针调用的成本,但您将在其他地方付出代价,唯一的问题是您在哪里支付:-)(这里没有足够的上下文)
    • 多态性的好处是付出的代价是语言而不是你。使用模板,您可以(例如)支付维护费用。作为您决定的程序。
    • 在计算资源方面,在这种情况下,如果我们能够证明臃肿代码的影响不会干扰代码的性能,那么基于策略的设计将花费更少以适应缓存。在这种情况下没有足够的膨胀担心,所以这更便宜。
    • 说真的,不管你能多快调用你将在网络上等待的方法。即使你有一个超级超快的网络连接,vtable 间接也不会引起注意。
    【解决方案4】:

    我认为,至少在这种特殊情况下,在多态性或模板专业化之间进行选择的要点是,如果您想选择在运行时或编译时使用哪种行为。
    例如,如果您想基于提供给用户的连接字符串来建立 udp 或 tcp 连接,那么多态最适合您的需求;创建一个具体类,然后将其传递给处理指向基接口的指针的通用代码。
    否则,您可能会考虑使用模板 - 我不确定您是否需要模板专业化。

    希望这会有所帮助:)

    【讨论】:

    • 这种情况下我想在编译时选择,系统不会在运行时改变,我希望创建高效的代码,虽然据说代码的大小可能是较大。也同意基于策略的设计是我一直在寻找的术语。
    • 你能澄清一下你所说的“高效”代码是什么意思吗?代码大小?执行时间处理时间?记忆?看来您在这里进行了过早的优化。据我所知,普通的旧虚拟函数可以完成这项工作......
    • 好吧,基于策略的设计代码大小可能会更大,我并不关心代码大小,因为它与任何缓存拟合问题无关。
    • 我不完全相信这是过早的优化。我知道该服务将用于需要超低延迟的超高频网络。我知道您可以争辩说,与其他成本相比,虚函数调用的成本相对较小。
    • 也就是说,如果绝对不需要在运行时更改协议,并且系统需要按照使用付费的理念进行设计,那么基于策略的设计似乎是必要的。
    猜你喜欢
    • 2014-10-08
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    相关资源
    最近更新 更多