我在 5 年多前发现了模板元编程,并从阅读现代 C++ 设计中获得了巨大的乐趣,但我从未找到在现实生活中使用它的机会。
您是否曾经在实际代码中使用过这种技术?
Boost的贡献者无需申请;o)
我曾经在 C++ 中使用模板元编程来实现一种称为“符号扰动”的技术,用于处理几何算法中的退化输入。通过将算术表达式表示为嵌套模板(即基本上通过手动写出解析树),我能够将所有表达式分析交给模板处理器。
用模板做这种事情比使用对象编写表达式树并在运行时进行分析更有效。它更快,因为修改后的(扰动的)表达式树可以在与您的其余代码相同的级别上提供给优化器,因此您可以获得优化的全部好处,无论是在您的表达式中,还是(在可能的情况下)在您的表达式和周围的代码。
当然,您可以通过为您的表达式实现一个小型 DSL(领域特定语言)并将翻译后的 C++ 代码粘贴到您的常规程序中来完成同样的事情。这将为您带来所有相同的优化优势并且更加清晰 - 但权衡是您必须维护解析器。
我发现现代 C++ 设计中描述的策略在两种情况下非常有用:
当我开发一个组件时,我希望它会被重用,但方式略有不同。Alexandrescu 提出的使用策略来反映设计的建议在这里非常适合 - 它帮助我解决了过去的问题,例如“我可以使用后台线程来执行此操作,但如果以后有人想在时间片中执行此操作怎么办?” 好的,我只是编写我的类来接受 ConcurrencyPolicy 并实现我目前需要的。然后,至少我知道支持我的人可以在需要时编写并插入新策略,而无需完全重新设计我的设计。警告:有时我必须控制自己,否则这可能会失控——记住YAGNI原则!
当我试图将几个类似的代码块重构为一个时。通常代码会被复制粘贴和稍微修改,因为否则它会有太多的 if/else 逻辑,或者因为涉及的类型太不同。我发现策略通常允许一个干净的万能版本,而传统逻辑或多重继承则不允许。
我在游戏图形代码的内部循环中使用了它,您需要某种程度的抽象和模块化,但无法支付分支或虚拟调用的成本。总的来说,它是一个比大量手写特殊情况函数更好的解决方案。
模板元编程和表达式模板作为优化方法在科学界变得越来越流行,它们将一些计算工作卸载到编译器上,同时保持一些抽象。生成的代码较大且可读性较差,但我已使用这些技术来加速 FEM 库中的线性代数库和求积方法。
对于特定于应用程序的阅读,Todd Veldhuizen是该领域的知名人士。一本流行的书是Daoqi Yang 的C++ and Object Oriented Numeric Computing for Scientists and Engineers。
在编写 c++库时,模板元编程是一种奇妙而强大的技术。我在自定义解决方案中使用过几次,但通常不太优雅的旧式 c++ 解决方案更容易通过代码审查并且更容易为其他用户维护。
但是,在编写可重用的组件/库时,我已经从模板元编程中获得了很多成果。我说的不是 Boost 的一些大的东西,只是会经常重用的小组件。
我将 TMP 用于单例系统,用户可以在其中指定他们想要的单例类型。界面非常基本。在它下面由重型 TMP 提供动力。
template< typename T >
T& singleton();
template< typename T >
T& zombie_singleton();
template< typename T >
T& phoenix_singleton();
另一个成功的用途是简化我们的 IPC 层。它是使用经典的 OO 风格构建的。每条消息都需要从一个抽象基类派生并覆盖一些序列化方法。没什么太极端的,但它会生成很多样板代码。
我们向它扔了一些 TMP,并自动生成了仅包含 POD 数据的简单消息案例的所有代码。TMP 消息仍然使用 OO 后端,但它们大大减少了样板代码的数量。TMP 还用于生成消息访问者。随着时间的推移,我们所有的消息都迁移到了 TMP 方法。为消息传递构建一个简单的 POD 结构并添加使 TMP 生成类所需的几行(可能是 3 行)比派生新消息以通过 IPC 发送常规类更容易且代码更少框架。
我一直使用模板元编程,但在 D 中,而不是 C++。C++ 的模板元语言最初是为简单的类型参数化而设计的,几乎是偶然地成为图灵完备的元语言。因此,它是只有 Andrei Alexandrescu 而不仅仅是凡人可以使用的 Turing tarpit。
另一方面,D 的模板子语言实际上是为超越简单类型参数化的元编程而设计的。Andrei Alexandrescu似乎很喜欢它,但其他人实际上可以理解他的 D 模板。它也足够强大,有人在其中编写了一个编译时光线跟踪器作为概念证明。
我猜我在 D 中写过的最有用/最重要的元程序是一个函数模板,它给定一个结构类型作为模板参数和一个列标题名称列表,其顺序与作为运行时的结构中的变量声明相对应参数,将读入一个 CSV 文件,并返回一个结构数组,每行一个,每个结构字段对应一列。所有类型转换(字符串到浮点数、整数等)都是根据模板字段的类型自动完成的。
另一个很好的,主要工作,但仍然不能正确处理少数情况,是正确处理结构、类和数组的深拷贝函数模板。它仅使用编译时反射/自省,因此它可以与结构一起使用,与成熟的类不同,它在 D 中没有运行时反射/自省功能,因为它们应该是轻量级的。
大多数使用模板元编程的程序员通过 boost 之类的库间接使用它。他们甚至可能不知道幕后发生了什么,只知道它使某些操作的语法变得更加容易。
我已经将它用于 DSP 代码,尤其是 FFT、固定大小的循环缓冲区、hadamard 变换等。
对于那些熟悉 Oracle 模板库 ( OTL )、boost::any 和Loki库(现代 C++ 设计中描述的库)的人,这里是概念证明 TMP 代码,它使您能够在容器中存储一行 otl_stream 并按vector<boost::any>
列访问数据数字。“是的”,我将把它合并到生产代码中。
#include <iostream>
#include <vector>
#include <string>
#include <Loki/Typelist.h>
#include <Loki/TypeTraits.h>
#include <Loki/TypeManip.h>
#include <boost/any.hpp>
#define OTL_ORA10G_R2
#define OTL_ORA_UTF8
#include <otlv4.h>
using namespace Loki;
/* Auxiliary structs */
template <int T1, int T2>
struct IsIntTemplateEqualsTo{
static const int value = ( T1 == T2 );
};
template <int T1>
struct ZeroIntTemplateWorkaround{
static const int value = ( 0 == T1? 1 : T1 );
};
/* Wrapper class for data row */
template <class TList>
class T_DataRow;
template <>
class T_DataRow<NullType>{
protected:
std::vector<boost::any> _data;
public:
void Populate( otl_stream& ){};
};
/* Note the inheritance trick that enables to traverse Typelist */
template <class T, class U>
class T_DataRow< Typelist<T, U> >:public T_DataRow<U>{
public:
void Populate( otl_stream& aInputStream ){
T value;
aInputStream >> value;
boost::any anyValue = value;
_data.push_back( anyValue );
T_DataRow<U>::Populate( aInputStream );
}
template <int TIdx>
/* return type */
Select<
IsIntTemplateEqualsTo<TIdx, 0>::value,
typename T,
typename TL::TypeAt<
U,
ZeroIntTemplateWorkaround<TIdx>::value - 1
>::Result
>::Result
/* sig */
GetValue(){
/* body */
return boost::any_cast<
Select<
IsIntTemplateEqualsTo<TIdx, 0>::value,
typename T,
typename TL::TypeAt<
U,
ZeroIntTemplateWorkaround<TIdx>::value - 1
>::Result
>::Result
>( _data[ TIdx ] );
}
};
int main(int argc, char* argv[])
{
db.rlogon( "AMONRAWMS/WMS@amohpadb.world" ); // connect to Oracle
std::cout<<"Connected to oracle DB"<<std::endl;
otl_stream o( 1, "select * from blockstatuslist", db );
T_DataRow< TYPELIST_3( int, int, std::string )> c;
c.Populate( o );
typedef enum{ rcnum, id, name } e_fields;
/* After declaring enum you can actually acess columns by name */
std::cout << c.GetValue<rcnum>() << std::endl;
std::cout << c.GetValue<id>() << std::endl;
std::cout << c.GetValue<name>() << std::endl;
return 0;
};
对于那些不熟悉提到的库的人。
operator >>
OTL 的 otl_stream 容器的问题在于,通过声明适当类型的变量并以下列方式应用到 otl_stream 对象,只能按顺序访问列数据:
otl_stream o( 1, "select * from blockstatuslist", db );
int rcnum;
int id;
std::string name;
o >> rcnum >> id >> name;
这并不总是很方便。解决方法是编写一些包装类并用来自 otl_stream 的数据填充它。希望能够声明列类型列表,然后:
olt_stream::operator >>(T&)
您可以借助 Loki 的Typelist
结构、模板特化和继承来完成所有这些工作。
借助 Loki 的库结构,您还可以生成一堆 GetValue 函数,这些函数返回适当类型的值,从列号(实际上是 in 类型的数量Typelist
)推导出来。
问这个问题将近 8 个月后,我终于使用了一些 TMP,我使用接口的TypeList以便在基类中实现 QueryInterface。
我将它与 boost::statechart 一起用于大型状态机。
是的,当我将遗留 API 包装在更现代的 C++ 接口中时,主要是做一些类似于鸭子打字的事情。
不,我没有在生产代码中使用它。
为什么?
不要那样做。其背后的原因如下:根据模板元编程的性质,如果您的逻辑的某些部分是在编译时完成的,那么它所依赖的每个逻辑也必须在编译时完成。一旦你启动它,在编译时执行你的逻辑的一部分,没有回报。雪球会继续滚动,没有办法阻止它。
例如,您不能迭代 boost::tuple<> 的元素,因为您只能在编译时访问它们。您必须使用模板元编程来实现原本简单直接的 C++,而当 C++ 的用户不够小心,没有在编译时移动太多东西时,总是会发生这种情况。有时很难看出编译时逻辑的某种使用何时会出现问题,有时程序员渴望尝试测试他们在 Alexandrescu 中读到的内容。无论如何,在我看来,这是一个非常糟糕的主意。
直到最近,由于对编译器的支持很差,许多程序员很少使用模板。然而,虽然模板在过去有很多问题,但较新的编译器有更好的支持。我编写的代码必须与 Mac 和 Linux 上的 GCC 以及 Microsoft Visual C++ 一起工作,而且只有 GCC 4 和 VC++ 2005 才能使这些编译器很好地支持该标准。
通过模板进行通用编程并不是您一直需要的东西,但绝对是您工具箱中有用的代码。
显而易见的示例容器类,但模板也可用于许多其他事情。我自己工作的两个例子是: