不过, 这篇文章的重点并非讲述 C++ 中关于复制构造函数的优化, 而是为了说明 C++0x 标准中出现的 Move Semantic 所试图解决的问题之一, 所以对关于复制构造函数的例子有大致理解即可.
重点是, 如果希望完整地编译文中最后一部分中出现的任何 0x 标准相关的代码, 强烈推荐 GCC 4.5.x 版本编译器.
废话少说, 先来一段代码:
#include <iostream>
struct cp_ctor_doubling {
cp_ctor_doubling(int ii)
: i(ii)
{}
cp_ctor_doubling(cp_ctor_doubling const& rhs)
: i(rhs.i * 2)
{}
int const i;
};
cp_ctor_doubling create(int i)
{
return cp_ctor_doubling(i);
}
int main()
{
cp_ctor_doubling c(create(10));
std::cout << c.i << std::endl;
return 0;
}
嗯, 也许有得到 B 和 C 的, 得到 D 的同学也许该试着为自己的编译器报个 bug 了, 如果得到 A, 那么恭喜, 您的编译器已经具备对于复制构造函数最基本的优化能力了.
C 语言忠实信徒揶揄 C++ 编译器会背着程序员做的 "额外的事情" 时, 历来会想到 C++ 的类复制构造函数. 然而, 现实不仅仅是这样, 编译器还会做出更多额外的事情, 比如把不必要的复制构造函数调用优化掉, 或者更准确地说, 把一些临时对象给优化掉, 于是复制构造函数根本不会被调用到.
如果对上述例子中的编译优化有任何疑虑, 可以尝试下面这段代码
#include <iostream>
struct cp_ctor_not_impl {
cp_ctor_not_impl(int ii)
: i(ii)
{}
cp_ctor_not_impl(cp_ctor_not_impl const&);
int const i;
};
cp_ctor_not_impl create(int i)
{
return cp_ctor_not_impl(i);
}
int main()
{
cp_ctor_not_impl c(create(10));
std::cout << c.i << std::endl;
return 0;
}
怎么样, C++ 编译器不是一般折腾吧, 不理构造函数吧, 暗地里给集成一个, 声明一个吧, 它又熟视无睹, 真是傲娇属性.
其实复制构造函数这概念是相当扯淡的. 以物理学的观点来说, 不是什么东西都能复制的, 比如能量对象复制一个就导致能量不守恒了哦; 从计算机的角度来看呢, 互斥访问的文件对象似乎也不应该被复制吧. 既然根本概念都站不在脚, 从复制构造函数诞生这一天起, 注定悲剧的命运是必然的结果.
题外话, Java 在这一点上做得比 C++ 略好那么一点点, 仅仅只好一点点, 可以说在这方面设计也是相当扯淡的. Java 对象没有复制构造函数, 但有一个
clone()
方法. 但是, clone()
方法是 java.lang.Object
的方法, 不过所幸这个愚蠢的接口是 protected
修饰的.如果复制构造函数不存在会怎么样呢? 世界会清净下来吗? 当然不会, 请试着将上面例子中的复制构造函数置于
private
控制之下, 结果是编译器在编译过程中就报错, 并且一定是 create
函数返回时错一次, main
函数中初始化对象再次错一次.这其中的隐情, 说到底是程序员, 无论是设计 C++ 的程序员还是使用 C++ 的程序员, 将复制构造函数的两个功能混为一谈而导致的. 此两个功能, 其中之一, 是基本的复制需求, 不可否认, 确实有一些情况下需要复制一些东西, 比如要弄个字符串副本进行操作, 又不想破坏原来的那个, 好说, 这个复制构造可以有; 至于另一个, 是跟名字对不上号的功能, 这里举个例子, 看看
std::auto_ptr
的实现就能明白了.auto_ptr(auto_ptr& a) throw();
这是 std::auto_ptr
的复制构造函数声明, 是不是看起来很别致呢? 问题出在参数不是 const
修饰的, 那么直接明说了, 被用来构造的那个 auto_ptr
在构造结束后, 所含指针会被置为空指针. 例如下面这个代码片段std::auto_ptr<int> a(new int(10));
std::auto_ptr<int> b(a);
++*a;
auto_ptr
之所以会有这么扭曲的设计, 无疑是为了最开始举的例子中可能发生的乱数: 有的编译器 (或链接器) 会优化掉临时对象, 进而不掉用复制构造函数, 而为了兼容那些可能不执行这些优化的后进生, 临时的 auto_ptr
必须将内含的指针转移, 而非复制, 给被构造的 auto_ptr, 这样当临时变量销毁时才不会出现副作用.当然, 这样胡来必然会有极大的代价, 那就是, 任何一本合格的 C++ 书籍中都应该提到的,
auto_ptr
此生与容器无缘.说到这里, 终于要进入正题了: C++0x 之 Move Semantic. 办事拖沓的标准委员会开始尝试解决这个问题, 于是引入了让人瞠目结舌的语法试图区分复制构造和, 新出现的, 转移构造 (姑且这么叫吧, 目前还没找到权威的中文译法).
废话少说, 还是先来一段代码:
#include <iostream>
struct movable {
movable(movable&& other) // HERE: Move Constructor
: p(other.p)
{
other.p = NULL;
}
explicit movable(int* pp)
: p(pp)
{}
int value() const
{
return *p;
}
~movable()
{
delete p;
}
private:
int* p;
movable(movable const&);
};
movable create(int i)
{
return movable(new int(i));
}
int main()
{
movable m(create(100));
std::cout << m.value() << std::endl;
return 0;
}
另外, C++ 为了弥补一直以来偷偷合成复制构造函数犯下的错误, 在新标准中提供了称为 "删除函数 (deleted function)" 的机制. 上述例子中, 只是将复制构造函数给私有化了, 从根本上不允许复制构造可以这么做
struct movable {
movable(movable&& other)
: p(other.p)
{
other.p = NULL;
}
explicit movable(int* pp)
: p(pp)
{}
int value() const
{
return *p;
}
~movable()
{
delete p;
}
private:
int* p;
movable(movable const&) = delete;
};
movable
的实例就只能够转移, 而不能复制, 非常安全. 当然, 不需要担心返回值优化, 编译器仍然能够正常地识别出哪些临时对象是不必创建的, 省去这些转移构造.写到这里, 留下一个坑先. 既然是转移构造函数, 不免会问个问题, 转移的来源是什么? 并不是所有东西都能够当作转移来源的, 比如下面的代码会报出错误
movable a(new int(100));
movable b(a);
b
的构造匹配了复制构造函数, 并且 b
的构造不可以匹配转移构造函数. 要讲类型的匹配可能又得一篇文章了, 所以这里先挖着, 有空再来补.不过, 我觉得这仍然是标准的失误. 我所希望的是,
a
可以 "转移" 为 b
, 然后立马安插 a
的析构代码, 并让 a
从符号表中消失 (类似 python 中手动 del
一个变量一样).最后介绍一个新标准替代
std::auto_ptr
的东西 (支持新标准的编译器会在遇到 std::auto_ptr
时给出 deprecated 警告), 那就是 std::unique_ptr
, 可以往容器里面放, 还可以用如下形式存入数组std::unique_ptr<int[]> array_int(new int[10]);
Tips: 删除函数还有一个用途, 子类禁用父类的非虚函数
#include <iostream>
struct base {
void echo()
{
std::cout << "base::echo" << std::endl;
}
};
struct inherit
: public base
{
void echo() = delete;
};
int main()
{
inherit i;
i.echo(); // error
return 0;
}