我很乐意解释我是如何写就 DumpEnum 的,甚至给出代码。此间我还将回答第二个问题。不过首先让我为其他读者解释一下 DumpEnum 是做什么的。在四月发表的文章中,我要做的一件事情是写一个 C++ 枚举来完全充当 .NET 框架类型 RegexOptions。RegexOptions 是一个枚举类型,你可以用 Regex::Match 和 Replace 之类的方法来控制匹配。例如,你可以用 RegexOptions::IgnoreCase 来调用 Regex::Match 以忽略大小写,或者
RegexOptions::Singleline 来将输入串作为一行。RegexOptions 的值如 Figure 1 所示。
为了从本机C++代码传递 RegexOptions,你需要一个具有相同值的 C-式样枚举。如果你仅仅是一时之需,那么编写这样的代码最快的方法是从文档中将之拷贝到你的 C++ 文件,然后按照 C 的语法编辑它。但如果你要进行印刷,或修改选项,或者想要包装若干枚举类型的话,那么最好最可靠的方法是写一个工具,它能自动生成 C++ 代码——尤其反射使之更容易;.NET 框架提供了全部描述自身所需的信息。所以我写了一个小程序 DumpEnum,你可以从命令行运行它,像这样:
DumpEnum RegexOptions
DumpEnum 将名字/值对写成 C/C++ 代码送到标准输出。你可以像下面这样将输出重定向到一个文件:
DumpEnum RegexOptions > regopt.h
然后将 regopt.h 插入到你的头文件。在我的文章中,RegexWrap.h 就是这么做的。DumpEnum 为 RegexOptions 实际生成的文件如 Figure 2 所示。
现在你知道 DumpEnum 是做什么的了,下面你会明白我是如何实现它的。
每一个框架类都是由 System.Type 类描述。它具备 Name 属性以获取类型名,IsEnum 告之该类型是否为枚举。DumpEnum 要做的第一件事情是不论什么类型名,都要获得 Type,把它们传递到命令行——例如,RegexOptions,实现它并不是想像的那么容易。获得 Type 最普通的两个方法是:如果已知对象实例,则调用 obj->GetType;否则用 __typeof。例如:
#using <System.dll>
using namespace System::Text::RegularExpressions;
...
Type *t = __typeof(RegexOptions);
在 C++ 中使用托管类型必须用 __typeof,因为 typeof 已经有定义(在新的 C++\CLI 中要用 typeid<>)。但 __typeof 只能用于符号名,而不是字符串,这意味着编译时你得知道名字,以及哪个程序集和其所属的名字空间。DumpEnum 在编译时不具备这些信息,所以要从命令行参数中获得类型名。DumpEnum 也不具备对象实例,所以它无法调用 Object::GetType,怎么办呢?
还有另一个得到 Type 的方法。如果已知该类型属于哪个程序集,那么可以加载它并调用 Assembly::GetType,它有一个串参数。但 Assembly::GetType 需要完整的类型名,并且你得知道要加载哪个程序集。DumpEnum 给我的第一个痛是要我在命令行敲入下面这些信息:
DumpEnum System.dll System.Text.RegularExpressions.RegexOptions
你知道 RegexOptions 在 System.dll 中,而且你也知道名字空间是 System.Text.RegularExpressions,因为文档就是这么说的。看文档是一件很恼人的事,而且敲入这个完整的名字空间足以让你得上腕关节综合症。我宁愿花三小时写一个只需敲12个字符而不是54个字符的程序,而不原意花30秒钟来查找DLL/namespace和敲入完整类型名。这似乎与懒惰者有些自相矛盾,但这是好程序员的精神特质所需要的,因为你最终是要做出一个很棒的工具永久使用——并且能够与朋友们分享!
在按照我的想法写 DumpEnum 之前,必须解决几个基本问题,其中包括本文的第二个提问:已知类型名(可能是非限定名),如:RegexOptions,如何知道哪个框架 DLL 包含该类型?没有集中的数据库或保存此信息的注册表项(这也是一件好事情),或方法调用,看来是没有希望了。但是等一等——再想想!所有的框架 DLLs 都在一个文件夹中,我最近一次数过也就七十来个。为什么强力使用之呢?只要将每个程序集加载到框架环境,查找到那个名字与期望的类型名匹配者。这样很简单直白,并且在我低档的 1 GHz P3 上只需几秒钟。我写了一个程序叫 FindType 就是做此事的:
FindType MenuItem
如图 Figure 3 所示。FindType 列出所有名字中带 MenuItem 类型输出的框架 DLLs。FindType 查找包含该单词文本的类型。换句话说,如果你运行“FindType Control”,那么 FindType 将输出 System.Windows.Forms.Control 和 System.Web.UI.Control,但不包括 System.Web.UI.WebControls.WebControl。这个思路假设你输入的是简称名字,也就是你在 #using 指令中使用的名字空间。FindType 通过建立“\bControl\b”这样的正则表达式来实现。特殊的锚点字符(原子零宽度断言)''\b''用于单词分隔,而不用再匹配中包含分隔符。
Figure 3 FindType 的运行结果
FindType 是如何知道要搜索哪个 DLLs 呢?所有框架 DLLs 都在一个名叫“C:\WINDOWS\Microsoft.NET\Framework\v1.1.4322”的文件夹中。你可以通过环境变量 %FrameworkDir% (实际解析出来是“C:\WINDOWS\Microsoft.NET\Framework”)和 %FrameworkVersion%(“v1.1.4322”)来获取自己机器上实际路径。为了简单起见,我在 CFindType 类中将此过程进行了封装。为了使用这个类,你得派生自己专用的特例并实现虚拟函数 OnMatch:
class CMyFindType : public CFindType {
protected:
virtual BOOL OnMatch(LPCTSTR typName, LPCTSTR asmPath)
{
// print it
}
};
然后实例化并调用 FindExact 方法:
CMyFindType ft;
int nFound = ft.FindExact("MenuItem");
CFindType 遍历程序集,查找与传入的名字相匹配的类型。只要找到一个匹配类型,便用该类型和程序集名调用你的虚拟 OnMatch 处理例程。如果你想用不同的正则表达式,你可以调用 CFindType::Find 代替 FindExact。CFindType 类使 FindType 程序的实现变得容易:只要写一个 OnMatch,将信息输出到标准输出 stdout 即可。具体细节请参考本文附带的源代码。
CFindType 本身派生于一个更为一般的类,CEunmTypes,它枚举框架中所有的类型,针对每个类型调用虚拟 OnType 函数。CFindType::OnType 用 Regex 比较类型名和请求的名字,并调用 OnMarch 检查其是否匹配。CEnumTypes 用 _tgetenv 来获得环境变量以建立型如“FrameworkDir\FrameworkVersion\*.dll”的文件规范,然后用 MFC 的 CFileFind 类枚举 DLLs。CEunmTypes 试图将每个 DLL 作为程序集加载。如果加载失败(也许该 DLL 不是一个托管程序集),CEnumTypes 则忽略它并继续搜索。如果加载成功,便调用 Assembly::GetExportedTypes 来获取程序集输出的 Type 数组,然后针对每个 Type 调用 OnType。代码如 Figure 4 所示。
现在有了 CFindType,我终于可以用它来解决最初的问题:修改 DumpEnum,以便我不用非得告诉它程序集和完整的类型名。DumpEnum 以不同的 OnMarch 处理例程使用 CFindType。DumpEnum 中的处理例程检查该类型确实在枚举的类型中(Type::IsEnum 返回 True),如果真是如此,则按照 C++ 代码方式取出枚举名/值对,如果 Figure 2 所示。DumpIt 函数实际的工作内容如 Figure 5 所示。DumpIt 用 Enum::GetUnderlyingType 获取枚举的底层类型(一般是 System.Int32),用 Enum::GetValues 获取枚举值,用 Convert::ChangeType 将枚举值转换为其对应的底层类型。以下是输出显示名字/值对的代码:
Type* entype = // 托管枚举类型
Array* values = Enum::GetValues(entype);
for (int i=0; iLength; i++) {
Object* enval = values->GetValue(i);
Object* unval = Convert::ChangeType(enval, untype);
_tprintf(_T("\t%s = %s,\n"), enval, unval);
}
现在你一举两得:FindType 使你能找到某个特定类型包含在哪个 DLL/程序集;DumpEnum 生成 C/C++ 代码来包装框架枚举类型。
继续讨论之前,本文有一个小插曲,我写完 FindType 之后,发现一个令人沮丧的事情,在 Visual Studio .NET 中已经附带有一个 FindType 程序,甚至名字都一样!微软版本的 FindType 所做的事情和我的相同,并且做得更好。(老天啊,微软的老大们捷足先登;我晕倒!)
微软的 FindType 具备选项来指定完全或部分匹配要搜索的目录和名字空间,是否显示方法,属性,事件等等。Figure 6 是它列出所有选项的帮助屏幕:
Figure 6 Visual Studio 附带的 FindType
如果你想了解更多关于反射,程序集和类型的内容,或者自在周五晚上无所事事,FindType 是个很好的学习研究例子。你可以在 VS.NET\SDK\v1.1\Samples\Applications\TypeFinder 目录找到它的源码,它是用 C# 和 Visual Basic 写的。我生成了 C# 版本,并将其 EXE 文件拷贝到我的 bin 目录;现在我都是用 FindType 来查找类型的。
是不是有了微软的 FindType,我自己的版本就毫无用处了呢?当然不是。例如,CFindType 和 CEnumTypes
是类,不是程序,也就是说你可以用它们来写自己的查找类型信息的工具以及像 DumpEnum 这样的程序。况且我的类是用中最好的编程语言 C++ 写的。而微软的版本用的则是 Visual Basic 和 C#。如果你决定在自己打大应用程序中使用 CFindType,那么应该注意到 CFindType 不用在枚举时考虑卸载程序集。因为每个程序集都会消耗相当多的内存,你可能会考虑修改这个实现,将每个程序集加载到其自己的应用程序域中,然后在完成程序集的搜索之后卸载应用程序域。
我正在用一个基于模板的库源代码,该库包含一些针对特定类型的模板函数特化。类模板,函数模板和模板函数特化都在头文件中。我在我的.cpp文件中 #include 头文件并编译链接工程。但是为了在整个工程中使用该库,我将头文件包含在 stdafx.h 中,结果出现特化模板函数的符号多重定义错误。我要如何组织头文件才能避免多重符号定义错误?我用 /FORCE:MULTIPLE,但我想用一个更好的解决方法。
Lee Kyung Jun
实际上,确实用更好的解决方法。稍后我会解释,但首先让我重温一下模板函数特化是如何工作的。假设你有一个比较两个基于 operator> 和 operator== 对象的模板函数:
template <typename T>
int compare(T t1, T t2)
{
return t1==t2 ? 0 : t1 > t2 ? 1 : -1;
}
该模板根据地一个参数是否等于、大于、或小于第二个参数而分别返回零或+/-1。它是典型的用于集合排序时的排序函数。它假设类型 T 具备 operator== 和 operator> 操作,并支持 int,float,double 或 DWORD 类型。但它不能应用于比较自负串(char* 指针),因为这个函数比较的是串指针,而不是字符串本身:
LPCTSTR s1,s2;
...
int cmp = compare(s1,s2); // s1<s2? Oops!
为了能进行字符串比较,你需要一个使用 strcmp 或其 TCHAR 版本 _tcscmp 的模板特化:
// specialization for strings
template<>
int compare<LPCTSTR>(LPCTSTR s1, LPCTSTR s2)
{
return _tcscmp(s1, s2);
}
没错,这样做完全正确,现在的问题是:将这个特化放在何处?显然是要放在模板的头文件中。但这样会导致符号多重定义的错误,就像 Lee 遇到的那样。原因很明显,模板特化是一个函数,而非模板。它与下面的写法是一样的:
int compare(LPCTSTR s1, LPCTSTR s2)
{
return _tcscmp(s1, s2);
}
没有理由不在头文件中定义函数——但是一旦这样做了,那么你便无法在多个文件中 #include 该头文件。至少,肯定会有链接错误。怎么办呢?
如果你掌握了模板函数特化即函数,而非模板的概念,你就会认识到有三个选项,完全与普通函数一样;特化为 inline,extern 或者 static。例如,像下面这样:
template<>
inline int compare<LPCTSTR>(LPCTSTR s1, LPCTSTR s2)
{
return _tcscmp(s1, s2);
}
对于大多数模板库而言,这是最容易和最常见的解决方案。因为编译器直接扩展内联函数,不产生外部符号,在多个模块中 #include 它们没有什么问题。链接器不会出错,因为不存在多重定义的符号。对于像 compare 这样的小函数来说,inline 怎么说都是你想要的(它更快)。
但是,如果你的特化很长,或出于某种原因,你不想让它成为 inline,那要如何做呢?此时可以做成 extern。语法与常规函数一样:
// in .h header file
template<>
extern int compare<LPCTSTR>(LPCTSTR s1, LPCTSTR s2);
当然,你得在某个地方实现 compare。部分细节如 Figure 7 所示。我在单独的模块 Templ.cpp 中实现了特化,它与主工程链接。Templ.h 被 #include 在 stdafx.h 中,而 stdafx.h 又被 #include 在 Templ.cpp 和主模块两个文件中——生成工程没有链接错误。去下载源代码自己尝试一下吧。
如果你正在为其他开发人员写模板库,extern 方式会很不爽,因为你必须创建一个带目标模块的链接库(lib),它包含有特化。如果你已经有了一个这样的 .lib,也没什么;如果没有,你可能会想方设法避免引入这样的库。仅用头文件实现模板是更好的方法(麻烦少)。最容易的方式是用 inline,此外,你还能将你的特化放在单独的头文件中,使之与其声明分开并要其他开发人员只在一个模块中 #include 特化。还有一个可选的方法是将所有东西放在一个文件中,并用预处理符号控制实例化:
#ifdef MYLIB_IMPLEMENT_FUNCS
template<>
int compare<LPCTSTR>(LPCTSTR s1, LPCTSTR s2)
{
return _tcscmp(s1, s2);
}
#endif
使用该方法,所有模块都包含此头文件,但在包含它之前,只有一个 #define MYLIB_IMPLEMENT_FUNCS。这个方法不支持预编译头,因为编译器用 stdafx.h 中的任何 MYLIB_IMPLEMENT_FUNCS 值加载预编译版本。
避免符号多重定义错误的最后同时也是用得最少的一个方法是将特化做成 static:
template<>
static int compare<LPCTSTR>(LPCTSTR s1, LPCTSTR s2)
{
return _tcscmp(s1, s2);
}
这样链接器也不会出错,因为静态函数不向外界输出其函数,并且它让你将所有东西都保持在一个头文件中,不用引入预处理符号。但它缺乏效率,因为每个模块都有一个函数拷贝。如果函数小到没什么——那为何不用内联呢?
所以简言之:将特化做成 inline 或 extern。通常都是用 inline。两种方法都得编辑头文件。如果使用的是第三方的库没有头文件,那么你除了用链接选项 /FORCE:MULTIPLE 之外别无选择。在你等着生成你的工程时,你可以告诉编写库文件的那个家伙——为什么要将函数模板特化定义成 inline 或者 extern。就说是我说的。
您的提问和评论可发送到 Paul 的信箱:cppqa@microsoft.com.