C++在运行时查找类型

3

我有一个由一些模板参数参数化的类:

template<typename Scalar, typename Integrator, int Dimension>
class foo;

模板参数中的每个参数都可以是几种可能类型之一。目前在 typedef foo<...> foo_type 中使用的 foo 类型是硬编码的。我希望调整我的程序,以支持一组 foo;类似于以下内容:

if (desired_foo_str == "2DSimpleFloat")
{
    foo<float,Simple,2>(params).method();
}
else if (desired_foo_str == "3DSimpleDouble")
{
    foo<double,Simple,3>(params).method();
}
else
{
    std::cout << "Unsupported foo."
}

foo的接口不依赖于其模板参数。我的问题是如何改进这个解决方案?我知道boost::mpl提供了类型向量,但它似乎更适用于编译时减少而非运行时切换。
澄清一下,假设我的程序接受用户提供的N维点集并对其进行积分(这只是一个简化),某些维数组合、积分方法和标量类型可以通过SIMD加速(因此使用模板参数)。所有foo的组合都是有效的,但不同的用户(他们都会编译我的程序)只需要为其工作指定几个特定的专业化。我希望能够:
$ integrate --method=2DSimpleFloat mypoints2d.dat
$ integrate --methid=3DSimpleDouble mypoints3d.dat

在运行时选择他们想要使用的方法,我想知道什么样的框架最适合让我将类型与字符串关联起来,以便更好地处理上述情况。


1
确定 foo 的类型必须是字符串比较吗?什么是 params?它包含任何决定模板参数的信息吗? - Ray
参数对于所有的模板参数都是相同的,不会决定它们。由于我需要根据用户提供的输入在运行时选择要使用的“foo”类型,所以它可能必须是一个字符串。 - Freddie Witherden
仅为解决手头的这个问题,而不管是否有更好的实现方式:针对您支持的类型进行部分模板特化?否则,这似乎需要使用Alexandrescu的类型列表... - ta.speot.is
阅读您的澄清证实了我对使用的怀疑。我会将N-D、任何类型的情况编码为模板,然后覆盖可以使用SIMD指令集的情况。也许将这些实现包装在#define中以支持跨平台(准备在其他CPU上编译您的程序,如ARM)。 - centaurian_slug
@centaurian_slug SIMD相关的内容都由我使用的向量数学库处理了。我的问题是用户如何在运行时指定他们想要从编译到程序中的哪个模板特化。在这个上下文中,“不支持”意味着“未编译到程序中”。 - Freddie Witherden
好的。我猜你正在使用模板,因为你有一些可重复使用的设置代码来格式化库调用的数据。当然,运行时选择需要某种表格或基于“if”的分派。 - centaurian_slug
3个回答

2
您可以创建一个默认方法的模板,该方法会抛出错误,并为您支持的每种组合创建模板特化。
class Simple {};
template<typename Scalar, typename Integrator, int Dimension>
class foo
{
public:
  void method();
  foo() {}
};

// default implementation throws an error
template<typename Scalar, typename Integrator, int Dimension>
void foo<Scalar,Integrator,Dimension>::method() {  cout << "unsupported\n"; };

// override default for supported cases:-
template<>
void foo<double,Simple,2>::method() { cout <<"method1\n"; };

template<>
void foo<double,Simple,3>::method() { cout <<"method2\n"; };

// test program
void main() {
   foo<float,Simple,2> a; a.method(); // output "unsupported"
   foo<double,Simple,2> b; b.method(); // output "method1"
   foo<double,Simple,3> c; c.method(); // output "method2"
}

你应该能够在类中自由地混合通用实现和特殊目的的覆盖(例如,一些渗透可能可以使用SIMD指令处理或其他方式)。
如果所有类方法都是相同和通用的,则限制使用的便捷方法可能是限制构造函数,以便不能实例化不需要的情况。
通常,如果正确使用重载和模板机制,您应该能够避免在使用它们的地方手动检查类型。这可以在编译时静态链接工作,而无需任何指针或虚拟调度。
如果支持的实现相同,则覆盖可以是包装器,直接指向另一个建议的模板方法。

1

你的问题没有提供足够的信息来完整回答,但我有一个预感:也许你应该考虑重构你的代码,以便将独立于参数的部分与依赖于模板参数的代码分离。

典型的例子来自Scott Meyers的书。假设你有一个正方形矩阵乘法器,并且你将其写成一个完整的模板:

template <typename T, unsigned int N>
Matrix<T, N> multiply(Matrix<T, N>, Matrix<T, N>)
{
   // heavy code
}

通过这种设置,编译器将为每个大小值N生成一个单独的代码片段!那可能是很多的代码,而所有N提供的只是循环中的一个限制。

因此,建议将编译时转换为运行时参数,并将工作负载重构为一个单独的函数,并仅使用模板存根来分派调用:

template <typename T>
void multiply_impl(unsigned int N,
                   MatrixBuf<T> const & in1, MatrixBuf<T> const & in1,
                   MatrixBuf<T> & out)
{
   // heavy work
}

template <typename T, unsigned int N>
Matrix<T, N> multiply(Matrix<T, N> const & in1, Matrix<T, N> const & in1)
{
  Matrix<T, N> out;
  multiply_impl(N, in1.buf(), in2.buf(), out.buf());
}

您可以尝试类似的方法:将所有与参数无关的代码放在一个基类中,并使派生类成为模板。运行时可以使用工厂函数创建正确的具体实例。作为继承的替代方案,还可以制作一个类型擦除包装器类,其中包含一个私有指向基类的指针,运行时会用具体的派生实现实例填充它。

0

我猜你是在寻找注册模式。这只是我的草稿,所以不要依赖它。

class AbstractFooFactory
{
     virtual AbstractFoo* create( ParamsType cons& params ) = 0;
     // or construct on stack and call .method()
     virtual void createAndCallMethod( ParamsType cons& params ) = 0;
};

class FooRegister
{
   ~FooRegister(); // delete all pointers
   template< typename FooFactory >
   void operator() ( FooFactory const & factory ) // for boost::mpl:for_each 
   { map[factory.getName()]= new FooFactory( factory ); }
   AbstractFooFactory* get( std::string name );
   std::map< std::string , AbstractFooFactory* > map;
};

template< typename Scalar, typename Integrator, typename Dimension >
class FooFactory: public AbstractFooFactory
{
    typedef FooFactory<Scalar, Integrator, Dimension > type; // Metafunction
    std::string getName(); // this will be a bit hard to implement
    AbstractFoo* create( ParamsType cons& params );
    void createAndCallMethod( ParamsType cons& params );
};

简单的轨迹可以用于存储类型名称:

template< typename Type >
struct NameTrails
{
    static const char const* value;
};

template<> const char const* NameTrails<int>::value = "Int";
template<> const char const* NameTrails<float>::value = "Float";
template<> const char const* NameTrails<double>::value = "Double";
template<> const char const* NameTrails<Simple>::value = "Simple";
template<> const char const* NameTrails<Complex>::value = "Complex";

template< typename Scalar, typename Integrator, typename Dimension >
std::string FooFactory::getName() 
{
  return boost::lexical_cast<std::string>( Dimension::value ) + "D"
         + NameTrails< Integrator >::value
         + NameTrails< Scalar >::value;
}

现在,您需要使用mpl::for_each注册所有类型:

FooRegister fooRegister;

typedef boost::mpl::vector<Simple,Complex> IntegratorsList;
typedef boost::mpl::vector<int,float,double> ScalarsList;
typedef boost::mpl::range_c<int,1,4> DimensionsList;

typedef boost::mpl::vector< 
     boost::mpl::vector< Simple, float, boost::mpl::int_<2> >,
     boost::mpl::vector< Simple, double, boost::mpl::int_<3> >,
    ... other types or full cross join ... > FooList;

boost::mpl::for_each< FooList, boost::mpl::quote3<FooFactory> >( 
    boost::ref(fooRegister) );

我不知道的是如何交叉连接 IntegratorsList,ScalarList,range_c<int,1,4> 来构建完整的 FooList。

fooRegister.get("2DSimpleFloat")->createAndCallMethod(params);

你可能想要以静态方式实现这个,所以是可以的,但我发现要比一个简单的动态映射或哈希映射实现更好的性能相对困难。


作为一种简化,只需要几个特定的方法,而不是所有可能的组合。 - Freddie Witherden
所以我认为使用if树是尽力使它尽可能简单的最佳方法。您可以将static std::string getName();放在foo内部。或者也许可以使用预处理器? - Arpegius

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接