应用程序配置文件变量






3.18/5 (8投票s)
2004年4月13日
8分钟阅读

45876

795
使用“一行代码”实现对只读或读/写配置文件的变量进行类型安全访问。
引言
这个小巧的工具是一个小的C++实用程序,它可以轻松地创建和访问应用程序配置文件变量。在大多数情况下,将这些值存储在注册表中可能会更有效率,但对于这个特定的项目,配置文件必须在异地生成并上传到运行应用程序的远程机器上。
在开发过程中,每次我们需要一个新变量时,我们都必须生成代码来声明它、初始化它、保存它并访问它。换句话说,整个过程都有一个例程,如果遗漏了任何一步,都可能导致不必要的调试工作。
如果生成新变量只需要写一行代码,那不是很棒吗?
问题定义
- 生成新的应用程序配置变量,编写“一行代码”,它将执行以下操作:
- 按名称声明变量。
- 声明变量的类型。
- 包含默认值(以防在文件中找不到)。
- 指定分配给变量的访问权限,即只读或读/写,以及
- 生成有关变量的信息,以供异地维护。
- 通过声明一个类型与变量名相似的局部变量,并使用C++重载运算符对局部变量进行操作来访问配置变量的值。这将带来以下好处:
- 如果我尝试将值赋给一个只读配置变量,编译器会捕获到这一点。
- 如果我尝试将错误类型赋给一个变量,编译器也会捕获到这一点。
例如:
//…somewhere in the project // (“One –line-of-code”)DECL_APPVAR_GET(const char*, // MyVar, “I have no imagination.”) // …elsewhere in the project void func() { MyVar var; // compiler error – read-only (GET) var = “Wait a minute… I felt something.”; // compiler error – wrong type BOOL eg = var; // OK to read const char* AfxMessageBox(var); }
- 流式传输变量。
解决方案
我开始查看我们想要用来访问变量的语法。我看到了两种可能性:
CAppVar::MyVar var; // 或者
CAppVar<MyVar> var;
起初,我觉得第二种语法看起来最有吸引力。然而,因为它是一个模板,序列化会有点困难。我想让解决方案尽可能简单,所以我选择了第一种语法。
接下来是“一行代码”宏,它将生成我想要避免的冗长输出。
显然,这需要一些思考。就C++而言,思考体现在类中,所以我们需要:
- 包装器类 - 此对象将包含单个变量的信息,以及流式传输信息和读/写其值的能力。
- 访问器类 - 此类将用于通过声明的访问权限和类型在应用程序的任何位置访问变量的值。
- 初始化器类 - 此对象将在创建时自动初始化所有必要的信息。
- 容器类 - 此对象将容纳我们所有的变量。
我们的“一行代码”宏将为我们生成大部分代码并相应地分发信息。
警告:有些程序员认为这种宏编码风格难以阅读和维护。虽然我并不完全同意,但我确实认为,如果您理解底层代码以及为什么使用宏来实现它,那么就没有理由不以这种方式使用它们。宏就是为此而设计的,因此命名为“MACROS”,而不是“DEFINES” ;)
让我们从包装器类开始,看看我们可以从中获得什么信息。
包装类
包装器类名为var_info
,它维护有关特定变量的信息。此类模仿模板类,可以通过两种方法访问数据:
Simple
- 其中数据假定为非指针类型,需要进行地址转换。String
- 其中数据假定为指针,不需要地址转换。
该类不是模板的原因在上面已简要提及,可以总结为RAM数据存储复杂度的微小增加,而流式传输和磁盘存储的复杂度大大降低。
该类的数据成员如下:
class var_info : public CObject { DECLARE_SERIAL(var_info) friend class CAppVar; protected: // used for lookups CString name; // just for show CString type; // this is only used for offsite reading BOOL write; // this is only used for offsite reading // real info and data int size; void* data;
目前,所有查找都是按名称进行的。这不是最高效的方式,但在这个例子中,我不想在声明宏中提供另一条信息(键)。如果需要,请随时更改。此外,列表是使用CObList
实现的。别笑,这是我之前想着可能需要它的序列化服务留下的。它将被移除。
该类利用了MFC的序列化技术(未来项目包括移植到WTL - 当需要时)。
可以通过以下成员和运算符来管理对数据的访问:
// operators inline operator int() { return _simple(int, data); } inline operator const char*() { return _string(const char*, data); } inline void operator=(UINT d) { copy((void*)&d, sizeof(UINT)); } inline void operator=(int d) { copy((void*)&d, sizeof(int)); } void operator=(const char* d) { data = (d) ? copy((void*)d, strlen(d)+1):NULL; size = (data) ? size:0; } … // general access inline const char* GetName() { return name; } inline const char* GetType() { return type; } inline int GetSize() { return size; } inline BOOL AllowWrite() { return write; } … // serialization void Serialize(CArchive& ar) { if(ar.IsStoring()) { ar << name << type << write << size; ar.Write(data, size); } else { ar >> type >> write >> size; del(data); data = alc(size); ar.Read(data, size); } } … // helper CString ToString(); // generates a string rep. for the value (needs a better approach) void ToValue(const char* val); // as above but opposite direction. protected: void* copy(void* d, int s) { if(s > size) { del(data); data = alc(s); } // del() == free() && alc() == malloc() memcpy(data, d, s); size = s; return data; }
该类相当直接,功能最少。通常,没有人会直接访问此类。任何访问都通过访问器类进行管理,访问器类会根据为该变量指定的访问权限来访问此类。在我的例子中,我已将对此类的访问提供给任何需要它的人(例如,枚举所有可用的应用程序变量,而不是特定的变量访问)。
接下来是容器类。
容器类
容器类充当所有应用程序变量的包装器。它维护一个var_info
结构的内部静态列表,并能够将这些变量与流式数据匹配。它还充当我们访问器类的命名空间,并作为应用程序变量创建和初始化的调用机制。换句话说,通过创建此对象的实例,我们的数据将立即可用。注意:每个应用程序只有一个实例,请。
为了实现这一点,任何新的应用程序变量都必须将其“一行代码”添加到此类的主体中。这是其声明两个变量的代码:
class CAppVar : public CObject { // {{ Global Config Variables DECL_APPVAR_SETGET(BOOL, MyVar1, FALSE) DECL_APPVAR_GET(const char*, MyVar2, _T("I have no imagination.")) // }} public: CAppVar() {} virtual ~CAppVar(); DECLARE_SERIAL(CAppVar) void Serialize(CArchive& ar); public: static CObList m_variableList; static var_info* Fetch(const char* name, bool assert = true); };
这个特定的例子有两个配置变量:
MyVar1
- 读/写MyVar2
- 只读
应用程序负责在需要时调用序列化函数。所有变量在容器创建时都存储在m_variableList
成员中。
警告:您不能将CAppVar
对象映射到顶层堆栈帧来进入应用程序,即,它不能是应用程序对象的直接成员,也不能声明为全局或静态对象。在这种情况下,必须使用堆分配(即,指针和new
运算符)。这是由于静态列表和初始化器成员之间的构造顺序。
另外两个类,访问器和初始化器,是由宏生成的。对于上面的例子,代码将展开显示以下CAppVar
类(除了一些可能的复制错误)。粗体斜体项代表宏参数。
class CAppVar : public CObject
{
// {{ Global Config Variables
// accessor for MyVar1
class MyVar1
{
var_info* info;
public:
MyVar1 ()
{ info = Fetch(“MyVar1”); ASSERT_VALID(info); }
void inline operator=(BOOL val)
{ ASSERT_VALID(info); (*info) = val; }
inline operator BOOL()
{ ASSERT_VALID(info); return (BOOL)(*info); }
inline const char* GetName()
{ ASSERT_VALID(info); return info->GetName(); }
inline const char* GetType()
{ ASSERT_VALID(info); return info->GetType(); }
inline BOOL AllowWrite()
{ ASSERT_VALID(info); return info->AllowWrite(); } \
};
private:
// initializer class
class CMyVar1
{
bool dummy;
public:
CMyVar1 ()
{ var_info* inf = new var_info(“MyVar1”, “BOOL”, TRUE);
(*inf) = TRUE; m_variableList.AddTail((CObject*)inf); }
} mMyVar1;
// accessor for MyVar2
class MyVar2
{
var_info* info;
public:
MyVar2 ()
{ info = Fetch(“MyVar2”); ASSERT_VALID(info); }
inline operator const char*()
{ ASSERT_VALID(info); return (const char*)(*info); }
inline const char* GetName()
{ ASSERT_VALID(info); return info->GetName(); }
inline const char* GetType()
{ ASSERT_VALID(info); return info->GetType(); }
inline BOOL AllowWrite()
{ ASSERT_VALID(info); return info->AllowWrite(); }
};
private:
// initializer class
class CMyVar2
{
bool dummy;
public:
CMyVar2 ()
{ var_info* inf = new var_info(“MyVar2”,
“const char*”, FALSE);
(*inf) = "I have no imagination.";
m_variableList.AddTail((CObject*)inf); }
} mMyVar2;
// }}
public:
CAppVar() {}
virtual ~CAppVar();
DECLARE_SERIAL(CAppVar)
static void Serialize(CArchive& ar);
public:
static CObList m_variableList;
static var_info* Fetch(const char* name, bool assert = true);
};
如上所述,概念很简单。无非是将类型声明包装在var_info
对象周围,并带有一些默认参数和运算符。只读声明缺少赋值运算符,因此如果进行任何尝试,将生成编译错误。
访问器只是一个公共类型声明,其名称与配置变量相同,而初始化器类是成员声明,其构造函数在CAppVar
对象创建时被调用。此构造函数创建一个新的var_info
对象,其中包含宏中提供的信息,并将其添加到CAppVar
对象的内部列表中。在我的例子中,没有人可以访问此变量或其类型定义 - 因为没有这个要求。
初始化器对象包含一个虚拟变量,以防编译器尝试优化代码。
我希望您能看到如何使用相关变量的局部声明来访问变量的内容。例如:
CAppVar::MyVar1 var; var = TRUE; // This will set the config variable MyVar1 to TRUE const char* szTemp = var; // This will generate a compile error //…
示例
提供的源代码包含一个示例,其中使用了相同的机制来创建这些变量的简单编辑器。有一个名为OnExampleCode()
的函数,它没有入口点,但演示了访问器类的用法。我尽量使代码尽可能简单,而不过多嵌入MFC。
此代码和示例是极简实现。它们被创建为原型代码,仅此而已。未经广泛测试,仅作为“空白页”提供。
该示例允许您更改配置变量的值、保存它们或打开它们 - 尝试一下,并告诉我您遇到的问题。
结论
这是一个相对简单的解决方案,除了可以节省创建新应用程序变量(可以序列化)的时间,以及可能像我一样,从其他代码的错误中节省调试时间之外,没有其他真正价值。它在这两方面都极大地帮助了我们。唯一的问题是,每次添加一组新变量时通常都需要进行完整的重新编译 - 所以请提前计划。
可能也有类似的解决方案已经存在。我没有看到也没有听说过任何一个,所以如果存在,那么我相信我们所有人 - 即使是最倾向于通用的人 - 都曾在某个时候重新发明过某些东西。
另外,对于我的解决方案,我将在序列化之前添加一个加密层。我也可能将其移植到读取注册表项,或者添加一些功能来将变量声明为注册表或文件。
作者:Alon Azar。
... [F5]