37

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

解决方案 1:使用接口

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

这只是一个简单的示例来说明我的问题。在现实世界中,接口将具有多个功能,一个类可能(并且将会!)实现多个接口。

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

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

4

7 回答 7

57

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

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

模板的主要优点是您可以使用不符合特定和精确继承层次结构的类型。例如,您可以将任何内容存储在可复制构造的向量中(在 C++11 中可移动构造),但 anint和 aCar在任何方面都没有真正的关联。这样,您可以减少与您的UseA类型一起使用的不同类型之间的耦合。

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

关于其他答案声称的表现,它们在某种程度上是正确的,但大多数都忽略了重点。选择模板而不是动态分派的主要优点不是动态分派的额外开销,而是编译器可以内联小函数这一事实(如果函数定义本身是可见的)。

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

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

以上所有内容都是挥手致意,并且与其他答案的方向相反(即,有些不精确可能会使差异看起来比实际要小,但现实比实际更接近这一点对一般答案动态调度会使你的代码变慢

于 2013-05-16T12:37:17.640 回答
23

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

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

但是,模板有其缺点

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

使用哪个取决于您的情况,并在一定程度上取决于您的偏好。模板化代码会产生一些迟钝的编译错误,这导致了诸如STL Error decrypt之类的工具。希望概念将很快实施。

于 2013-05-16T12:00:44.013 回答
18

模板案例的性能会稍好一些,因为不涉及虚拟调用。如果回调使用非常频繁,则支持模板解决方案。请注意,“非常频繁”直到每秒数千次才会真正生效,甚至可能更晚。

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

于 2013-05-16T11:48:31.120 回答
4

您可以考虑像合约这样的接口。从它派生的任何类都必须实现接口的方法。

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

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

于 2013-05-16T11:48:01.990 回答
1

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

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

于 2013-05-16T11:51:59.133 回答
0

我会选择模板版本。如果您从性能方面考虑这一点,那么这是有道理的。

虚拟接口 - 使用虚拟意味着该方法的内存是动态的并且在运行时决定。这具有开销,因为它必须查阅 vlookup 表以在内存中定位该方法。

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

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

于 2013-05-16T11:49:33.747 回答
0

选项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不是模板。 A并且A2没有通用的基接口类。

然而它确实有效。

于 2021-01-08T19:34:50.937 回答