关于 ATL 中 CComControl 的构造
分享一篇 C++语言 & ATL 的高阶解读笔记,你需要在C++语言特性中上串下跳,应该算篇有质量的文章。
class ATL_NO_VTABLE CHello :
// ...
public CComControl<CHello>
{
// ...
}
叶子类,继承CComControl,将自己作为模板参数传入。(ATL贯彻全场的静态多态技巧)
template <class T, class WinBase = CWindowImpl< T > >
class ATL_NO_VTABLE CComControl : public CComControlBase, public WinBase
{
public:
CComControl() : CComControlBase(m_hWnd) {}
// ...
}
CComControl有个默认模板参数CWindowImpl,而CWindowImpl也使用叶子类作为其模板参数。
在初始化列表中CComControlBase使用的m_hWnd来得有点妖怪:
m_hWnd是WinBase带来的(多重继承)
m_hWnd这时候没有 有效的值
因为 子对象CComControlBase 先于 子对象WinBase构造,而 子对象WinBase 只是分配了内存(见下条解释)
这里违背了“按声明次序使用”的原则,使用了没有初始化的成员,进入危险地带。
m_hWnd这时候 有 有效的内存空间。
因为叶子对象CHello已经分配了空间,正在通过 构造函数 建立各成员变量的初值。
尽管CComControlBase使用了一个没有初始化的、叶子类带来的成员变量,但小心使用依然有效。
class ATL_NO_VTABLE CComControlBase
{
public:
CComControlBase(HWND& h) : m_hWndCD(h)
{
memset(this, 0, sizeof(CComControlBase));
m_phWndCD = &h;
// ...
}
// ...
// Attributes
public:
#pragma warning(push)
#pragma warning(disable: 4510 4610) // unnamed union
union
{
HWND& m_hWndCD;
HWND* m_phWndCD;
};
#pragma warning(pop)
// ...
}
这里进入CComControlBase神奇的构造函数
它将自己(作为一个子对象)所拥有的内存清零:起始地址this,清零长度自己的大小sizeof(CComControlBase)
注意它的参数是一个引用,这里 传值 与 传引用 将有迥然不同的区别(而不是过去的差不多,主要是优化)
m_phWndCD赋值为 参数的地址。语言特性小考场:由于参数是引用,引用的取地址值,与引用所值对象地址一致。
回到CComControl构造函数初始化列表问题,子对象CComControlBase的m_phWndCD现在指向,另一个子对象WinBase中的m_hWnd成员。
设计讨论:CComControlBase为使用一个HWND参数,没有进一步使用模板,而是简单的使用一个成员变量。虽然CComControlBase子对象和WinBase子对象都住在一个家中——CHello叶子对象里,CComControlBase应有可能使用模板,进一步的将m_phWndCD所占内存开销去掉。为啥没有呢?是为了避免过多的纠缠、变复杂化(以易于独立思考、理解),还是HWND需要一定的独立性?
语言特性弯弯绕绕:再遇union的效用
语言书上说“引用一经复制,便不能修改”,幽暗的世界里这不是事实。此处应该使用了语言实现的一些底层特性,以及Windows编程时的一些命名约定。
匿名union让m_hWndCD与m_phWndCD共用一个内存,通过对m_phWndCD的修改,我们改变了引用m_hWndCD的值!
介于Windows编程的命名约定,我们习惯了m_pXXX == & m_XXX 这样的惯例,此处同样成立
&m_hWndCD == m_phWndCD
*m_phWndCD == m_hWndCD
有点抓狂,对同一个内存的两种访问方法,居然得到相同的值,更有甚者 m_hWndCD == m_hWnd,这绝对是对C++语言意志力的考验!