【问题标题】:virtual functions with compile time constants具有编译时常量的虚函数
【发布时间】:2016-01-07 15:33:28
【问题描述】:

我将首先提出我的问题,并在下面添加一些更长的解释。我有以下类设计,因为 C++ 不支持虚拟模板方法,所以它不起作用。我很乐意了解实现此行为的替代方案和解决方法。

class LocalParametersBase
{
public:
  template<unsigned int target>
  virtual double get() const = 0;   //<--- not allowed by C++
};

template<unsigned int... params>
class LocalParameters : public LocalParametersBase
{
public:
  template<unsigned int target>
  double get() const;               //<--- this function should be called
};

使用简单的函数参数代替模板参数目前是没有选择的,原因如下:

  1. 派生类中此方法的实现依赖于一些模板元编程(使用可变类模板参数)。据我所知,不能使用函数参数(即使它们是常量整数类型)作为模板参数。
  2. 仅使用编译时常量调用该方法。性能在我的应用程序中至关重要,因此我希望从编译时的计算中受益。
  3. 需要公共基类(为简洁起见,我省略了接口的其余部分)。

非常感谢任何建议。


更新:动机

由于对这种布局的动机有很多疑问,我将尝试用一个简单的例子来解释它。想象一下,您想测量三维空间中的轨迹,在我的具体示例中,这些轨迹是磁场中带电粒子(质量固定)的轨迹。您测量这些轨迹通过近似为 2D 表面的敏感探测器。在轨道与敏感探测器的每个交叉点,轨迹由 5 个参数唯一标识:

  • 两个 local 坐标,用于描述轨道与探测器表面 local 坐标系中表面的交点(这就是以这种方式选择类名的原因) ,
  • 指定轨迹方向的两个角度,
  • 一个包含粒子动量和电荷信息的参数。

因此,一条轨迹完全由一组五个参数(以及相关的表面)识别。但是,单个测量仅包含前两个参数(表面的局部 2D 坐标系中的交点)。这些坐标系可以是不同的类型(卡尔德斯坐标系、圆柱坐标系、球坐标系等)。因此,每次测量都可能会限制全部 5 个参数中的不同参数(或者甚至可能是这些参数的非线性组合)。然而,拟合算法(例如考虑一个简单的 chi2 最小化器)不应该取决于测量的特定类型。它只需要计算残差。看起来像

class LocalParametersBase
{
public:
   virtual double getResidual(const AtsVector& fullParameterSet) const = 0;
};

这很好用,因为每个派生类都知道如何将完整的 5 维参数集映射到其局部坐标系上,然后它可以计算残差。我希望这能解释一下为什么我需要一个通用的基类。还有其他与框架相关的原因(例如现有的 I/O 基础设施),您可以将其视为外部约束。
您可能想知道上面的示例不需要我询问的模板化get 方法。只有基类应该向用户公开。因此,如果您有一个 LocalParameterBase 对象列表并且您可以使用它们拟合轨迹,那将会非常混乱。您甚至可以获得测量的局部参数的值。但是您无法访问实际测量值的信息(这使得以前的信息无用)。

我希望这可以阐明我的问题。我感谢到目前为止收到的所有 cmets。


对于我当前的项目,我正在编写一个类,其主要目的是充当固定大小的稀疏向量的包装器。我的类没有存储整个向量(它是某些系统状态的表示),而是将大小减小的向量作为成员变量(= 对应于总参数空间的子域)。我希望下面的插图能让您了解我要描述的内容:

VectorType(5) allParameters = {0.5, 2.1, -3.7, 4, 15/9};   //< full parameter space
VectorType(2) subSpace      = {2.1, 4};                    //< sub domain only storing parameters with index 1 and 3

为了能够与原始向量建立连接,我需要“存储”复制到“缩短”向量中的索引。这是使用非类型可变参数模板参数实现的。我还需要能够用某个索引查询参数的值。如果此参数未存储在“缩短的”向量中,这将产生编译时错误。我的简化代码如下:

template<unsigned int... index>
class LocalParameters
{
public:
  template<unsigned int target>
  double get() const;

private:
  AtsVectorX m_vValues;
};

LocalParameters<0,1,4> loc;
//< ... do some initialisation ...
loc.get<1>();  //< query value of parameter at index 1
loc.get<2>();  //<-- this should yield a compile time error as the parameter at index 2 is not stored in this local vector class

我设法使用一些简单的模板编程来实现这种行为。但是我的代码的其他部分需要通过一个接口统一处理这些“缩短”的向量。我仍然希望能够通过接口LocalParametersBase访问是否存储了具有特定索引的参数的信息(如果没有我想得到编译时错误),如果是,我想访问的值这个参数。在代码中,这应该类似于

LocalParametersBase* pLoc = new LocalParameters<0,1,3>();
pLoc->get<1>();

【问题讨论】:

  • 总体来说,我觉得templates和virutal函数基本是对立的。模板 = 编译时多态性,虚函数 = 运行时多态性。在我看来,试图将两者结合起来听起来很糟糕。如果您确实需要该功能,请考虑将类本身而不是函数模板化。
  • 我不太理解你的解释,你为什么要使用继承?您是否正在考虑交换仅在运行时才知道的稀疏向量的不同实现?你说其他函数想要一个统一的接口,但是你展示的实现需要一个统一的接口,还有什么?
  • 举个具体的例子,pLoc-&gt;get&lt;1&gt;这行有严重的概念问题。您出于性能原因使用整数模板参数实现 get 并在编译时执行某些操作,但是您引入了一个直到运行时才能解决的间接寻址,而且启动起来也相当昂贵。
  • @DavidHaim 我知道这两个概念有不同的用途。我的问题是我知道 get() 函数只会使用编译时常量调用,我需要与派生类的可变参数模板参数进行比较。据我所知,如果我将 target 用作 get 方法的简单函数参数,这是不可能的。使基类模板化不是一种选择,因为例如我需要能够在向量中存储许多不同的实例。
  • @NirFriedman 我的应用程序是一个跟踪软件。一条轨迹可以用 5 个参数来描述,即总相空间。轨迹是从许多测量中重建的,但每次测量仅添加 1 或 2 个坐标的信息。对于轨道重建算法,我不感兴趣单个测量的具体坐标是​​什么(特别是因为数学总是相同的)。因此我需要一个抽象基类来描述“一些参数的测量”。

标签: c++ templates inheritance variadic-templates


【解决方案1】:

一个建议

如果没有关于你在做什么的更多信息,我只是对推动你采用这种方法的原因进行有根据的猜测。

依赖于虚拟接口的代码的一个常见性能问题是框架提供了以非常高的频率向虚拟方法分派的通用功能。这似乎是您面临的问题。您有对稀疏向量执行计算的代码,并且您想向它提供一个通用接口来表示您碰巧创建的每个稀疏向量。

void compute (LocalParametersBase *lp) {
    // code that makes lots of calls to lp->get<4>()
}

但是,另一种方法是通过使用模板参数来表示正在操作的派生对象类型,从而使计算变得通用。

template <typename SPARSE>
void perform_compute (SPARSE *lp) {
    // code that makes lots of calls to lp->get<4>()
}

compute 模板版本中的每个 get&lt;&gt; 调用都针对派生对象。这使得计算能够像您编写代码以直接操作 LocalParameters&lt;0,1,4&gt; 一样快,而不是在每次 get&lt;&gt; 调用时执行动态调度。

如果在执行计算时必须允许框架控制,因此在基类上执行计算,则基类版本可以分派给虚拟方法。

class ComputeBase {
public:
    virtual void perform_compute () = 0;
};

void compute (LocalParametersBase *lp) {
    auto c = dynamic_cast<ComputeBase *>(lp);
    c->perform_compute();
}

通过使用CRTP,您可以创建一个以派生类型为模板参数的帮助类,并通过传入派生来实现此虚拟方法。因此,计算只需要一次动态调度,其余的计算都是在实际的稀疏向量本身上执行的。

template <typename Derived>
class CrtpCompute : public ComputeBase {
    void perform_compute () {
        auto d = static_cast<Derived *>(this);
        perform_compute(d);
    }
};

现在你的稀疏向量派生自这个帮助类。

template <unsigned int... params>
class LocalParameters
    : public LocalParametersBase,
      public CrtpCompute<LocalParameters<params...>> {
public:
    template <unsigned int target> double get() const;
};

让你的界面按照你指定的方式工作

计算结果后,您希望将生成的稀疏向量放入容器中以供以后检索。但是,这不再是对性能敏感的操作,因此您可以使用下面描述的方法来实现。

基本模板方法
&rightarrow;基模板类虚方法
&rightarrow;派生模板方法

如果您希望使用多态,则将基类中的模板方法调用委托给虚函数。由于是模板方法,所以虚函数必须来自模板类。您可以使用动态转换来获取相应的模板类实例。

template <unsigned int target>
class Target {
public:
    virtual double get() const = 0;
};

class LocalParametersBase {
public:
    virtual ~LocalParametersBase () = default;
    template <unsigned int target> double get() const {
        auto d = dynamic_cast<const Target<target> *>(this);  // XXX nullptr
        return d->get();
    }
};

要自动执行每个Target 的虚拟方法,您可以再次使用CRTP,将派生类型传递给帮助程序。帮助器转换为派生类型以调用相应的模板方法。

template <typename, unsigned int...> class CrtpTarget;

template <typename Derived, unsigned int target>
class CrtpTarget<Derived, target> : public Target<target> {
    double get() const {
        auto d = static_cast<const Derived *>(this);
        return d->template get<target>();
    }
};

template <typename Derived, unsigned int target, unsigned int... params>
class CrtpTarget<Derived, target, params...>
    : public CrtpTarget<Derived, target>,
      public CrtpTarget<Derived, params...> {
};

现在,您可以从派生类适当地继承。

template <unsigned int... params>
class LocalParameters
    : public LocalParametersBase,
      public CrtpCompute<LocalParameters<params...>>,
      public CrtpTarget<LocalParameters<params...>, params...> {
public:
    template <unsigned int target> double get() const;
};

【讨论】:

  • 这里我不关注dynamic_cast的使用,你评论一下。我们知道Crtp 中的thisDerived 的基础,那么static_cast 会错过的runtine 类型检查是什么?哦,如果你使用动态转换,你应该检查指针是否为 nullptr。
  • 我认为它会捕获一个编码错误,其中错误的类型被传递为Derived。但是,无论如何,我正在考虑删除它。
  • @jxh 非常感谢您的建议和解释。您的代码示例完全解决了我的问题。我不得不承认它看起来比我所希望的要复杂。关于您的建议,我仍然有一个问题/问题:在执行性能敏感操作时,我只能通过基类指针访问稀疏向量(首先所有测量值都被收集、校准并存储在 LocalParametersBase 列表中指针,然后以该列表作为输入调用拟合算法)。在这种情况下,模板化计算方法不起作用?!
猜你喜欢
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 2010-09-12
  • 1970-01-01
  • 1970-01-01
  • 2014-02-10
  • 2014-06-30
相关资源
最近更新 更多