【问题标题】:boost::polycollection, std::variant, or CRTP?boost::polycollection、std::variant 还是 CRTP?
【发布时间】:2018-09-27 06:07:02
【问题描述】:

假设“标准”C++ 继承范式:

struct GeneralFunc
{
  /*..members..*/
  virtual double value(double a, double b) { return 0; }
};

struct Func_classA : GeneralFunc
{
  /*..members..*/
  double value(double a, double b) { return a * b; } 
};

struct Func_classB : GeneralFunc
{
  /*..members..*/
  double value(double a, double b) { return a + b; }
};

void main(){
  double a = 1.0, b = 1.0;
  std::vector<GeneralFunc*> my_functions;
  //fill my_functions from input
  for (auto& f : my_functions)
  {
    double v = f->value(a, b);
  }
}

我想要一个对迭代最有效的实现,即最小化间接引用、最大化内联优化等。为了限制问题,我事先知道我想要实现的每个特定“类型”(我可以只定义我需要的“func”类型,而不必允许其他可能性)。

出现几个可用的选项:

boost::polycollection

#include <boost/poly_collection/base_collection.hpp>
//...rest the same
boost::base_collection<GeneralFunc> my_functions
//...rest the same

std::variant

#include <variant>
//...rts
using funcs = std::variant<Func_classA, Func_classB /*..possibly more../*>
std::vector<funcs> my_functions

或 CRTP (Curiously Recurring Template Pattern) 让我知道正确的命名法,但在这里我根据“类型”“向上转换”基类——一种手动调度。

template<typename T>
struct GeneralFunc
{
  /*..members..*/
  int my_type;
  double value(double a, double b) {
    switch (my_type){
    case TYPE_A:
      return static_cast<Func_classA*>(this)->value(a,b);
  /*..you get the idea..*/

我可以牺牲边际效率来简化开发,但是对于这种情况下的“最佳实践”是否有共识?

EDITS* 修正了一些错别字;我目前的开发是 CRTP 最后一个选项的“开发中”。

解决方案:

经过测试,boost::polycollection 和 std::variant 都是有效的方法。然而,事实证明这是最有效的(根据记忆,可能略有偏差)。

enum ftype { A = 0, B, C };
struct GeneralFunc
{
  ftype my_type;
  GeneralFunc(ftype t) : my_type(t) {}
  inline double value(double a, double b) const; // delay definition until derived classes are defined
}

struct Func_classA : GeneralFunc
{
  Func_classA() : GeneralFunc(ftype::A) {}
  inline double value(double a, double b) const { return a * b; }
}
/* define B, C (& whatever) */

inline double GeneralFunc::value(double a, double b)
{
  switch(my_type){
    case (ftype::A):
      return static_cast<Func_classA*>(this)->value(a,b);
  /* same pattern for B, C, ect */
  }
}

void main(){
  std::vector<std::unique_ptr<GeneralFunc>> funcs;
  funcs.push_back(std::make_unique<Func_classA>());
  funcs.push_back(std::make_unique<Func_classB>());

  funcs[0]->value(1.0,1.0); // calls Func_classA.value
  funcs[1]->value(1.0,1.0); // calls Func_classB.value
}

【问题讨论】:

  • 你在你的基类中错过了 virtual
  • 通常会有其他约束导致一种或另一种设计。我认为您的原始解决方案已经非常理想。但是您可以将基类中的函数定义为pure virtual(至少应该是virtual)。而且,顺便说一句,您的最后一个示例不是 CRTP。
  • 最后,如果您关心的是速度/效率,那么标准答案是:实施,衡量!
  • 什么是 CTRP?是 CRTP 吗?
  • 为了我的钱,如果可能的话,我会选择变体。

标签: c++ boost std-variant


【解决方案1】:

我很想只使用std::function 作为容器,而不是重写它。

using GeneralFunc = std::function<double(double, double);

struct Func_classA
{
  /*..members..*/
  double value(double a, double b) { return a * b; } 
  /*explicit*/ operator GeneralFunc () const { return [this](double a, double b){ value(a, b) }; }
};

struct Func_classB
{
  /*..members..*/
  double value(double a, double b) { return a + b; }
  /*explicit*/ operator GeneralFunc () const { return [this](double a, double b){ value(a, b) }; } 
};

void main(){
  double a = 1.0, b = 1.0;
  std::vector<GeneralFunc> my_functions;
  //fill my_functions from input
  for (auto& f : my_functions)
  {
    double v = f(a, b);
  }
}

【讨论】:

    【解决方案2】:

    我认为您没有包含一个选项(这是我用于性能关键代码的选项),即创建一个函数对象的元组并“迭代”这样的元组。不幸的是,没有很好的 API 来迭代一个元组,所以必须实现他自己的。见下面的sn-p

    #include <tuple>                                                                                                                                                                                   
    #include <functional>                                                                                                                                                  
    
    template<int ... Id, typename Functions>                                                                                                                             
    auto apply(std::integer_sequence<int, Id ...>, Functions& my_functions, double& v, double a, double b){                                                          
        ([](auto a, auto b){a=b;}(v, std::get<Id>(my_functions)( a, b )), ...);                                                                                                                                     
    }
    
    int main(){                                                                                                                   
    auto fA = [](double a, double b){return a*b;};                                                                                    
    auto fB = [](double a, double b){return a+b;};                                                                                    
    //create the tuple
    auto my_functions=std::make_tuple(fA, fB);                                                                                                       
    double v=0;                                                                                                    
    double a = 1.;                                                                                                                      
    double b = 1.;
    //iterate over the tuple                                                                                                                                                                    
    apply(std::make_integer_sequence<int, 2>(), my_functions, v, a, b);                                                                                                                                                      
    

    }
    通过这种方式,您可以创建一个类型安全的零开销抽象,因为编译器知道您使用的类型的所有信息(您不需要任何类型擦除机制)。也不需要虚函数(与 CRTP 相同),因此编译器可能会内联函数调用。上面的 sn-p 使用 C++17 通用 lambda,也可以以 C++14 或 C++11 兼容的方式实现,但会更冗长。我更喜欢这个而不是 CRTP,因为对我来说它看起来更具可读性:没有静态转换到派生类,也没有人为的继承层次结构。

    编辑:从您的回答看来,您在这里并不需要 CRTP,您使用 CRTP 解决方案编写的内容与此等价

    enum ftype { A = 0, B, C };
    
    auto fA = [](double a, double b){return a*b;};
    auto fB = [](double a, double b){return a+b;};
    
    int main(){
    
    std::vector<ftype> types(2);
    types[0]=A;
    types[1]=B;
    
    auto value = [&types](double a, double b, ftype i){
        switch(i){
        case (ftype::A):
        return fA(a,b);
        break;
        case (ftype::B):
        return fB(a,b);
        break;
        }
    };
    
    double v=value(1., 1., A);
    v=value(1., 1., B);
    
    }
    

    可能是口味问题,但我认为上面的版本更具可读性(您实际上并不需要公共基类或派生类的静态转换)。

    【讨论】:

    • Op 不一定要所有函数都只需要一次
    • 这不是一个约束,可以改变“应用”来做其他事情,也取决于运行时参数。约束是容器大小和函数对象等信息必须在编译时可用。问题是 if 这样的信息在编译时可用,然后使用任何具有动态调度的东西(虚拟函数、std::variant、std::function、函数指针)都会增加开销。静态调度(std::tuple 和 CRTP)的解决方案是零开销,但它们要求容器的大小是已知的编译时间
    • 是的,但功能序列听起来是动态的,正如评论 //fill my_functions from input 所暗示的那样
    • 我的理解是调用函数的顺序可以是动态的,但是要从中选择的函数对象在编译时是已知的(否则 CRTP 也不起作用)。无论如何,@Christopher Mauney 可能需要澄清一下来解决这个问题..
    • 输入将来自文件中的参数列表,例如(1,1,a), (5,2,a), (3,6,b) 等等。这些的数量可以是100s到1000s。 (其实就是一个反应网络,我在计算反应率)
    猜你喜欢
    • 1970-01-01
    • 1970-01-01
    • 2017-03-05
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    相关资源
    最近更新 更多