c++接口与模板

C++ Interface vs Template

本文关键字:接口 c++      更新时间:2023-10-16

对于同样的问题,我有两个解决方案-从一个"控制器"到使用的对象进行某种回调,我不知道该选择什么。

方案一:使用接口

struct AInterface
{
    virtual void f() = 0;
};
struct A : public AInterface
{
    void f(){std::cout<<"A::f()"<<std::endl;}
};
struct UseAInterface
{
    UseAInterface(AInterface* a) : _a(a){}
    void f(){_a->f();}
    AInterface* _a;
};
解决方案2:使用模板
struct A
{
    void f(){std::cout<<"A::f()"<<std::endl;}
};
template<class T>
struct UseA
{
    UseA(T* a) : _a(a){}
    void f(){_a->f();}
    T* _a;
};
这只是一个简单的例子来说明我的问题。在现实世界中,接口将有几个函数,一个类可能(也将)实现多个接口。

代码不会被用作外部项目的库,我也不必隐藏模板实现——我这样说是因为如果我需要隐藏"控制器"实现,第一种情况会更好。

你能告诉我每种情况的优点/缺点以及使用哪种更好吗?

在我看来,性能应该忽略(不是真的,但微优化应该),直到你有一个理由。如果没有一些硬性要求(这是一个紧循环,占用了大部分CPU,接口成员函数的实际实现非常小……),如果不是不可能的话,将很难注意到差异。

所以我会专注于更高的设计水平。在UseA中使用的所有类型共享一个共同的碱基有意义吗?他们真的有关系吗?类型之间是否存在明确的 Is -a关系?那么OO方法可能会起作用。它们是无关的吗?也就是说,它们是否有一些共同的特征,但没有直接的is-a关系可以建模?使用模板方法。

模板的主要优点是,您可以使用不符合特定和精确继承层次结构的类型。例如,你可以在一个可复制构造的向量中存储任何东西(在c++ 11中是可移动构造的),但是intCar在任何方面都没有真正的联系。这样,您就减少了使用UseA类型的不同类型之间的耦合。

模板的缺点之一是,每个模板实例化都是不同的类型,与从同一基本模板生成的其余模板实例化无关。这意味着您不能将UseA<A>UseA<B>存储在同一个容器中,这会导致代码膨胀 (UseA<int>::fooUseA<double>::foo都是在二进制文件中生成的),更长的编译时间(即使不考虑额外的函数,使用UseA<int>::foo的两个翻译单元都将生成相同的函数,并且链接器将不得不丢弃其中一个)。

关于其他答案所声称的性能,他们在某种程度上是正确的,但大多数人都错过了重要的一点。与动态分派相比,选择模板的主要优点不是动态分派的额外开销,而是编译器可以内联小函数(如果函数定义本身是可见的)。

如果函数不是内联的,除非函数只需要很少的周期来执行,否则函数的总成本将超过动态调度的额外成本(即调用中的额外间接和在多重/虚拟继承的情况下this指针的可能偏移)。如果函数做一些实际的工作,并且/或者它们不能内联,它们将具有相同的性能。

即使在一种方法与另一种方法的性能差异可以测量的少数情况下(假设函数只需要两个周期,并且调度因此使每个函数的成本翻倍),如果该代码是80%的代码的一部分,占用的cpu时间少于20%,假设这段特定的代码占用了1%的CPU(这是一个巨大的数字,如果你考虑到为了获得显著的性能,函数本身必须只占用一两个周期!),那么你谈论的是1小时程序运行中的30秒。再次检查前提,在2GHz的cpu上,1%的时间意味着该函数必须每秒调用超过1000万次。

以上所有都是不确定的,它与其他答案的方向相反(即,有些不精确的地方可能会使差异看起来比实际情况小,但实际情况更接近于此,而不是一般答案动态调度会使您的代码变慢

)。

各有利弊。来自c++编程语言:

  1. 当运行时效率更高时,选择模板而不是派生类。
  2. 如果添加新的变体而不重新编译是很重要的,则选择派生类而不是模板。
  3. 当不能定义公共基时,首选模板而不是派生类。
  4. 当具有兼容性约束的内置类型和结构很重要时,首选模板而不是派生类。

然而,模板有它们的缺点

    使用面向对象接口的代码可以隐藏在。cpp/中。CC文件,当模板强制在头文件中公开整个代码时;
  1. 模板会导致代码膨胀;
  2. OO接口是显式的,只要对模板参数的需求是隐式的,并且只存在于开发人员的头脑中;
  3. 大量使用模板会影响编译速度。

使用哪一个取决于你的情况和你的喜好。模板化的代码可能会产生一些笨拙的编译错误,从而导致诸如STL错误解密之类的工具。希望这些概念很快就能实现。

模板的情况会有稍好的性能,因为没有涉及到虚调用。如果回调非常频繁地使用,那么最好使用模板解决方案。请注意,"极其频繁"直到每秒数千次才会真正生效,甚至可能更晚。

另一方面,模板必须在头文件中,这意味着对它的每次更改都将强制重新编译所有调用它的站点,不像在接口场景中,实现可能在.cpp中,并且是唯一需要重新编译的文件。

你可以把接口看作一个契约。从该接口派生的任何类都必须实现该接口的方法。

另一方面,

模板隐式地具有一些约束。例如,T模板参数必须有一个方法f。这些隐含的需求应该被仔细地记录下来,涉及模板的错误消息可能会很混乱。

Boost Concept可以用于概念检查,这使得隐式模板需求更容易理解。

您描述的选择是静态多态性与动态多态性之间的选择。如果你搜索它,你会发现很多关于这个话题的讨论。

对于这样一个笼统的问题,很难给出一个具体的答案。一般来说,静态多态可以提供更好的性能,但是由于c++ 11标准中缺少概念,这也意味着当一个类没有建模所需的概念时,您可能会得到有趣的编译器错误消息。

我会使用模板版本。如果你从性能的角度考虑这一点,那么它是有意义的。

Virtual Interface——使用Virtual意味着方法的内存是动态的,在运行时决定。这有开销,因为它必须查询vlookup表以在内存中找到该方法。

Templates——你得到静态映射。这意味着当你的方法被调用时,它不需要查阅查找表,并且已经知道该方法在内存中的位置。

如果你对性能感兴趣,那么模板几乎总是你的选择。

选项3如何?

template<auto* operation, class Sig = void()>
struct can_do;
template<auto* operation, class R, class...Args>
struct can_do<operation, R(Args...)> {
  void* pstate = 0;
  R(*poperation)(void*, Args&&...) = 0;
  template<class T,
    std::enable_if_t<std::is_convertible_v<
      std::invoke_result_t<decltype(*operation), T&&, Args&&...>,
      R>,
    bool> = true,
    std::enable_if_t<!std::is_same_v<can_do, std::decay_t<T>>, bool> =true
  >
  can_do(T&& t):
    pstate((void*)std::addressof(t)),
    poperation(+[](void* pstate, Args&&...args)->R {
      return (*operation)( std::forward<T>(*static_cast<std::remove_reference_t<T>*>(pstate)), std::forward<Args>(args)... );
    })
  {}
  can_do(can_do const&)=default;
  can_do(can_do&&)=default;
  can_do& operator=(can_do const&)=default;
  can_do& operator=(can_do&&)=default;
  ~can_do()=default;
  auto operator->*( decltype(operation) ) const {
    return [this](auto&&...args)->R {
      return poperation( pstate, decltype(args)(args)... );
    };
  }
};

现在你可以做

auto invoke_f = [](auto&& elem)->void { elem.f(); };
struct UseA
{
  UseA(can_do<&invoke_f> a) : m_a(a){}
  void f(){(m_a->*&invoke_f)();}
  can_do<&invoke_f> m_a;
};

测试代码:

struct A {
    void f() { std::cout << "hello world"; }
};
struct A2 {
    void f() { std::cout << "goodbye"; }
};
A a;
UseA b(a);
b.f();
A2 a2;
UseA b2(a2);
b2.f();

生活例子。

can_do上实现更丰富的多操作接口是一个练习。

UseA不是模板。AA2没有共同的基接口类。