Windows 注册表的模板 C++ 包装器
一个好的 C++ 包装类应该具备哪些属性?让我们以 Windows 注册表为例来形式化它。
理论要求和考虑
在各种 C++ 库、框架等中,有太多的 C++ 类用于访问 Windows 注册表。从用户的角度来看,它们都具有不同的属性。用户在使用它们时的舒适程度可能完全不同。
那么,一个通用的 C++ 类应该拥有什么样的接口才能方便地在程序中使用呢?让我们尝试形式化“舒适”类的基本属性。
- 它应该是一个值类。用户不应该被锁定于将其作为参数传递给函数、从中返回,以及自由地复制它等等。
- 它应该管理其底层的原始句柄、系统资源的指针,并处理所有引用计数和清理工作。用户不应该有可能因为错误而留下未关闭的句柄。
- 它应该尽可能使用标准类型、类、容器来承载数据。
- 它应该以标准方式报告错误——使用异常机制。一个类至少应该保证基本的异常安全。当然,最好是保证强的异常安全。(http://en.wikipedia.org/wiki/Exception_handling)。
对于表示某些外部系统的类,会添加以下属性。
- 用户可能可以访问底层句柄,但此类访问应在用户的代码中明确指定。说实话,对底层句柄的访问是设计接口不完整的标志。因此,代码中存在类似这样的类
operator HFONT() { return m_hFont; }
是通过间接转换错误地轻松违反类不变性的方法。
- 在用户可能从某个边界 API 获取原始句柄并成为其所有者的情况下,存在各种情况。或者他(她)需要将句柄发送到此类 API 并忘记它。对于此类操作,代码中应存在名为“attach”和“detach”的显式方法。
- 接口可能具有接受原始句柄作为输入的的方法。默认行为是不成为这些句柄的所有者,不释放它们,除非指定了某些显式标志,以防接口具有这些标志。
- 底层系统可能使用特定的系统数据类型来承载某些数据。一个类应该从内部数据类型到 C++ 程序数据类型有很强的映射。用户不应该有可能将底层类型隐式存储到不合适的 C++ 类型中。
- 如果底层系统允许枚举某些项,则此方面应转换为 C++ 世界中常用的迭代器。因此,用户可以间接使用系统的项在各种通用算法中。
此外,当然还有一些线程安全方面的问题。为简化起见,此处省略。文章中讨论的类作为示例,不是线程安全的。
类公共接口描述
Windows 注册表访问函数集有两个版本,像许多其他函数一样:ASCII 和 Unicode(更确切地说,多字节字符和宽字符)。因此,有几种利用它们的方法。有人可以为了简单起见,例如,只使用 ASCII 子集。另一方面,一整套函数可以被一个类包装。在这种特定情况下,选择了 C++ 标准库的方法。该类名为“basic_reg_key
”,它被编写成一个模板,以字符类型为参数,就像许多 STL 容器一样。因此,特定的模板实例化利用了特定的子集。
该类广泛使用文本字符串进行数据承载和指定键的名称。用户使用什么特定类型来承载文本?最通用的 C 风格的以 null 结尾的字符串和标准 C++ 库中的std::basic_string
对象。由于没有明显的选择,类中的文本字符串由模板参数表示,类使用简单的模板助手来获取实际的字符串数据。因此,用户可以指定“const CharT*
”类型或“std::basic_string<CharT>
”类型作为输入,假设“basic_reg_key
”的参数与此相同“CharT
”。
应该使用什么类型的异常来表示特定的错误类型?一种错误是对象使用不当。此类错误不依赖于外部条件,并且是稳定的,这意味着它们是可复现的。它们由标准类型“std::logic_error
”表示。例如,每次我们尝试使用未打开的键时,都会引发“std::logic_error
”。第二种是运行时错误。它们偶然发生,并取决于外部条件。用“std::runtime_error
”来表示它们是很自然的。有时此类错误可以携带底层系统(如果后者是它们实际的来源)的附加信息。在这种情况下,会引发后代“win_error
”。
“basic_reg_key
”类提供了强的异常保证。以下是其接口的描述。
声明
template <class CharT> class basic_reg_key;
该类设计为由“char
”或“wchar_t
”类型参数化。
构造函数
basic_reg_key(); template <class Str> basic_reg_key(HKEY parent_key, Str subkey_name, REGSAM access=KEY_ALL_ACCESS); template <class AnotherCharT, class Str> basic_reg_key(const basic_reg_key<AnotherCharT> &r, Str subkey_name, REGSAM access=KEY_ALL_ACCESS);
第一个方法构造一个未绑定的对象,该对象不代表任何注册表项。第二个方法以指定的名称和访问权限打开指定原始键下的子项。第三个方法使用另一个键类作为父级执行相同操作。在发生错误的情况下,第二个和第三个方法将引发“win_error
”类型的异常。
复制构造函数
basic_reg_key(const basic_reg_key &r); template <class AnotherCharT> basic_reg_key(const basic_reg_key<AnotherCharT> &r);
这两种方法都使用 WinAPI 的“DuplicateHandle
”函数复制系统句柄。复制的句柄与原始句柄具有相同的访问权限。在发生任何错误的情况下,这些方法都会引发“win_error
”。
常规打开器
template <class Str> void open(HKEY parent_key, Str subkey_name, REGSAM access=KEY_ALL_ACCESS); template <class AnotherCharT, class Str> inline void open(const basic_reg_key<AnotherCharT> &r, Str subkey_name, REGSAM access=KEY_ALL_ACCESS);
这些方法的工作方式与构造函数完全相同。用户只能对未绑定的对象调用它们。否则将引发“std::logic_error
”。
关闭器、析构函数
void close() throw(); ~basic_reg_key();
析构函数会自动调用“close
”方法。后者可以针对任何状态的对象调用。
void swap(basic_reg_key &r) throw(); basic_reg_key& operator=(const basic_reg_key &r); template <class AnotherCharT> basic_reg_key& operator=(const basic_reg_key<AnotherCharT> &r);
“swap
”方法使用指定对象交换系统句柄和状态变量。复制运算符使用“swap
”方法进行带有强异常保证的复制。
Getter
template <class Str> const DWORD get_DWORD( Str name, DWORD def, bool ignore_absence=false, bool ignore_wrong_type=false) const; template <class Str> const std::basic_string<CharT> get_REGSZ_or_EXPAND( Str name, const CharT* def=NULL, bool ignore_wrong_type=false, bool dont_expand=false) const; template <class Str> const std::wstring get_LINK( Str name, const wchar_t* def=NULL, bool ignore_wrong_type=false) const; template <class Str> multisz_ptr_t get_MULTISZ( Str name, bool ignore_absence=false, bool ignore_wrong_type=false) const; template <class Str> binary_ptr_t get_BINARY( Str name, bool ignore_absence=false, bool ignore_wrong_type=false) const;
假设数字或单个字符串是少量数据,并且可以按值返回。相反,多字符串或二进制数据可能相对较大,因此这些类型使用智能指针返回。所有这些方法都可能引发“win_error”。如果注册表值的实际数据类型不同,它们可能会引发“type_error”异常。用户可以使用标志“ignore_wrong_type”忽略此方面。如果用户指定了默认值或“ignore_absence”标志,在注册表中找不到实际数据的情况下,这些方法将返回默认值或空值。
Setters
template <class Str> void set_DWORD(Str name, DWORD value); template <class Str1, class Str2> void set_REGSZ_or_EXPAND( Str1 name, Str2 value, bool is_regsz=true); template <class Str1, class Str2> void set_LINK(Str1 name, Str2 value); template <class Str, class It> void set_MULTISZ( Str name, It begin, It end); template <class Str, class Ctr> inline void set_MULTISZ( Str name, const Ctr &c); template <class Str> void set_BINARY(Str name, const BYTE* begin, const BYTE* end); template <class Str> inline void set_BINARY(Str name, const std::vector<BYTE>& buffer);
当需要将多个字符串存储在 MULISZ 注册表值中时,可以将它们指定为通用容器或一对通用迭代器。要存储二进制数据,用户应指定指向二进制缓冲区开始和结束位置的两个原始指针,或者一个 std::vector<BYTE> 对象(更安全)。
template <class Str> basic_reg_key& delete_value(Str name, bool ignore_absence=false); template <class Str> basic_reg_key& delete_subkey(Str name, bool ignore_absence=false);
第一个方法删除指定名称的值。第二个方法递归删除指定的子项。如果值或子项不存在且标志具有默认值,则这些方法将引发“win_error”异常。这种设计允许编写简单的代码来删除一系列键,而无需担心它们是否存在。例如:basic_reg_key<char>(HKEY_CLASSES_ROOT, "").delete_subkey(".mp3", true).delete_subkey(".avi", true);
。
template <class Str> basic_reg_key create_subkey(Str name, bool is_volatile=false, REGSAM access=KEY_ALL_ACCESS) const;
该方法在当前键下创建子项,并返回代表新创建子项的新“basic_reg_key”对象。
basic_reg_key& attach(HKEY key); const HKEY detach();
“attach”方法关闭任何先前打开的句柄(如果已打开),并将指定的键与对象关联。“detach”方法仅断开内部原始句柄与对象的链接并返回原始句柄。
val_iterator begin_vals() const; val_iterator end_vals() const; range_vals vals() const;
前两个方法返回一个值的正向迭代器,该迭代器可用于遍历当前键的值。第三个方法返回一个具有“begin”和“end”方法的代理对象,这些方法返回相同的迭代器。解引用的迭代器返回一个 pair,其第一个值是以std::basic_string
存储的注册表值的名称,第二个值是DWORD
类型,表示值的类型。该 pair 具有到 std::basic_string 的转换运算符,该运算符返回值的名称。因此,有人可以写类似这样的内容:“std::string s = *it;
”
key_iterator begin_keys() const; key_iterator end_keys() const; range_keys keys() const;
与前几个方法相同,但返回子项名称的迭代器。使用它们,用户可以遍历子项的名称。解引用的迭代器返回 std::basic_string 对象。
此外,该类禁止与任何其他类型进行任何比较,因为这是无意义的。可以使用“operator unspecified_bool() const
”来测试该类是否代表实际键,该运算符使用“safe-bool”模式实现(http://www.artima.com/cppsource/safebool.html)。
简单的用法示例
假设我们需要创建一个子项并设置其特定值。这可以通过以下代码完成
reg_key(HKEY_CURRENT_USER, "Software") .create_subkey("my-subkey") .set_REGSZ_or_EXPAND("my-value", "this is the string");
临时对象在第一行创建。它在第二行用于创建并返回另一个代表子项的临时对象。最后一个对象在第三行用于设置值。
另一项任务可能是打印 HKEY_CLASSES_ROOT 中以“.c”开头的子项名称。此任务可以通过以下方式完成(除了 STL,使用 boost 的“filter_iterator”适配器)
using namespace boost; using namespace std; bool start_with_c(const string &s) { return s.find(".c") == 0; } // ... cout << "'.c*' classes root subkeys:" << endl; reg_key<char> k(HKEY_CLASSES_ROOT, "", KEY_READ); copy( make_filter_iterator(&start_with_c, k.keys().begin(), k.keys().end()), make_filter_iterator(&start_with_c, k.keys().end(), k.keys().end()), ostream_iterator<string>(cout, "\n")); cout << endl;
在附加的存档中可以找到更实际的样本应用程序。它已通过“mingw”编译器(gcc 版本 4.4)和 Visual Studio 2008 的 Microsoft 编译器成功构建。
源代码
“basic_reg_key
”类的实现可以在附加存档中找到,位于示例应用程序的源代码内。类本身实现在“win-registry.cpp”和“win-registry.h”文件中。该应用程序作为 Eclipse 项目交付。可以使用 Eclipse IDE 进行编译,也可以在命令行中使用“cmake”构建系统进行编译(http://www.cmake.org)。
如果您有任何问题、评论,请随时发送到您可以在源代码中找到的电子邮件地址。