原文出处:http://blog.csdn.net/vincent_lon/archive/2008/09/19/2950218.aspx
1>e:\program files\microsoft visual studio 9.0\vc\atlmfc\include\afxtempl.h(776) : error C2248: 'CObject::operator =' : cannot access private member declared in class 'CObject'
1> e:\program files\microsoft visual studio 9.0\vc\atlmfc\include\afx.h(562) : see declaration of 'CObject::operator ='
1> e:\program files\microsoft visual studio 9.0\vc\atlmfc\include\afx.h(532) : see declaration of 'CObject'
1> This diagnostic occurred in the compiler generated function 'CList<TYPE,ARG_TYPE> &CList<TYPE,ARG_TYPE>::operator =(const CList<TYPE,ARG_TYPE> &)'
1> with
1> [
1> TYPE=CProgram,
1> ARG_TYPE=CProgram &
1> ]
上面这段编译器报警是不是有似曾相识的感觉?想必很多人在用VC2005以及之后的版本的VC编译器时看到过这个东西,在google查一下error C2248: 'CObject::operator =' : cannot access private member declared in class 'CObject' 你会发现很多人碰到类似的问题。一位叫“中国民工”的blog中说明了引发这一问题的的原因,请参见http://www.cppblog.com/hlong/archive/2007/11/20/37015.html
根据民工兄的解释,由于我们的类中定义了CArray类型的成员,而CArray<A, A&>类的operator=是private类型(它继承自CObject::operator=,且被定义为private类型)。并给出了解决之道:如果我们的类/结构体中有CArray(或CList等其他的派生自CObject类)的成员变量,我们最好添加上一个public类型的operator=运算赋重载函数;
问题虽然是解决了,但是似乎还是没弄明白产生问题的根本原因:既然问题是由于CArray没有定义Operator=操作从而导致需要调用基类CObject中的私有Operator=操作而引起的,那么为什么微软要将CObject的Operator=操作定义成私有呢?或者说为什么CObject在实际上并没有对Operator=和拷贝构造函数做任何实质性的定义的情况下要去定义这两个函数,并且还将他们定义为private。。唯一的解释就是微软不希望继承自CObject的类使用默认的Operator=操作,或者说微软不希望使用了CArray或者CList或者CMap的Collections模板类的类使用默认的Operator=操作。答案的确是如此,不仅如此,对于默认的拷贝构造函数结论也是一样(可能有些人碰到到同样的编译器报错,不过是针对拷贝构造函数的,后面我会解释引发这两种类型的报错的真正原因)。
要想搞清楚这个问题,就先要了解C++编译器生成的默认函数——这些我们平时一直在使用却很少关心的幕后工作者到底是怎样工作的。当我们创建一个C++变量,为一个变量赋值,或者调用一个C++函数,或者对一个C++变量进行取地址&操作时,这些都是一些基本操作,我们总是能得到我们想要的结果(至少看起来是这样)因为这些都是C++的语义,而负责实现这些语义的是C++编译器。
编译器又是如何实现这些语义的呢,比如当你构造一个C++变量时,编译器会检查你有没有为这个变量的类型创建一个构造函数,如果没有,编译器会自己帮你生成一个默认的构造函数,从而实现了一个C++变量的构造;类似的,在用户执行其他一些诸如赋值操作,拷贝构造,取地址等基本操作时,编译器遵循同样的原则,即在用户未定义相应的操作情况下自动生成一个默认的操作;正因为如此,甚至很多程序员并不知道它们的存在(我记得几年前我去一家公司面试的时候,就被问及过这样一个问题:C++类的默认函数有几个,分别是什么,,其实这是一个蛮深刻的问题),即使知道它们,也会觉得在不需要我们付出任何关心的情况下,这些操作都是理所应当应该是正确的,但是事实并非总是如此。
Effective C++在条款45也对这几个函数进行了比较详细的分析;其中构造函数和析构函数其实就是一个空函数,什么也不做,所以当我们用VC也好或者其他的C++编译器创建一个空类也好,编译器都会为我们的类事先定义一个构造和析构函数,就是为了不让我们去使用默认的构造和析构函数。所以一般情况下我们很少会忽视这两个函数的存在,但是另外3个很重要的函数:拷贝构造和赋值操作符,以及取地址操作符就很容易被忽视掉,因为我们很少去实现这3个操作,大部分都是使用的编译器提供的默认的实现。
对于默认赋值操作符和拷贝构造函数的默认实现,Effective C++上的解释是:官方的规则是:缺省拷贝构造函数(赋值运算符)对类的非静态数据成员进行 "以成员为单位的" 逐一拷贝构造(赋值)。即,如果m是类C中类型为T的非静态数据成员,并且C没有声明拷贝构造函数(赋值运算符),m将会通过类型T的拷贝构造函数(赋值运算符)被拷贝构造(赋值)---- 如果T有拷贝构造函数(赋值运算符)的话。如果没有,规则递归应用到m的数据成员,直至找到一个拷贝构造函数(赋值运算符)或固定类型(例如,int,double,指针,等)为止。默认情况下,固定类型的对象拷贝构造(赋值)时是从源对象到目标对象的 "逐位" 拷贝。对于从别的类继承而来的类来说,这条规则适用于继承层次结构中的每一层,所以,用户自定义的构造函数和赋值运算符无论在哪一层被声明,都会被调用。
另外一个很重要的概念是:当且仅当相应的操作被定义时,编译器才为操作变量所属的类型生成对应的默认操作;也就是说,如果代码中没有出现相应的操作,编译器什么也不会做。
再回到我们一开始民工兄的例子中,假设我们的类C中定义了一个CArray类型的成员变量,并且C中没有显示的定义operator=操作(这就意味着编译器可能需要为C自动生成一个默认的operator=操作)根据上面的说法,当且仅当C的实例被赋值时编译器才会生成默认的operator=操作,而编译器报警告诉我们,编译器在生成operator=操作时遇到了障碍,这个障碍就是:C的CArray成员具有一个赋值运算符定义,但是该定义是私有的(实际上是CArray继承自CObject的operator=定义)。。我们看一下民工兄的例子的源码:
3 struct A
4 {
5 int a;
6 };
7
8 struct B
9 {
10 CArray<A, A&> b;
24 };
25
26 typedef CArray<B, B&> C;
27
28 void test()
29 {
30 B b;
31 C c;
32 c.Add(b);
33 }
从这段代码中可以看出,test中对B的实例b并没有做任何赋值的操作,为什么编译器会去实现B的默认operator=操作呢?
问题就处在CArray::Add()的函数签名身上,Add函数的源码如下
template<class TYPE, class ARG_TYPE>
AFX_INLINE INT_PTR CArray<TYPE, ARG_TYPE>::Add(ARG_TYPE newElement)
{ INT_PTR nIndex = m_nSize;
SetAtGrow(nIndex, newElement);
return nIndex; }
可见,形参newElement的类型是ARG_TYPE,ARG_TYPE其实就是TYPE的引用,在这里就是B的引用;答案似乎明了了:
因为Add函数的签名要求输入参数的类型为B&,所以当编译器编译语句:c.Add(b); 时会自动将b强制转换成B的引用,即将这条语句编译成:B& d = b; c.Add(d); 赋值操作就这样出现了!
为了验证这个想法,可以做另一个测试,调用一个传值的函数,这样就不需要做“B& d = b;”的转换了,那么就应该不会有问题了。
很不幸,实际上编译器会报同样的错误,只是现在不是operator=而是拷贝构造函数。
原因很简单,因为对于传值的方式,实际上就是“通过值来传递一个对象”,而这又是由这个对象的类的拷贝构造函数定义的。也就是说当通过传值的方式调用一个子程序时,最终是编译器通过调用拷贝构造函数来实现的。所以问题是一样的,因为类型B没有定义自己的拷贝构造函数,于是编译器试图生成一个默认的,但它同样遇到了障碍,因为CArray的拷贝构造也是private的(同样继承于CObject)。
至此,我们对于这个问题有了一个比较全面的认识,其实问题的本质在于:不论是默认的operator=还是默认的拷贝构造也好都是有其致命的缺陷的,正如Effective C++条款11中所描述的,当这些默认的实现在遇到需要动态分配内存的类的时候就会出现问题,也就是所谓的深拷贝问题。而Collections模板类就是典型的不能使用默认operator=和默认的拷贝构造的例子。
所以当我们自定义的类中包含Collections模板类的成员时我们就没法再偷懒使用编译器自动生成的operator=和的拷贝构造了,而必须自己动手实现,这也是微软想提醒我们的一个事实。