About
RSS

Bit Focus


C++ 转移构造

Posted at 2010-07-17 16:27:35 | Updated at 2018-08-15 15:43:20

    这篇文章中的例子均可在 GCC 4.5.1 版本和 Clang 2.8 版本编译, 在某些较老或优化不尽相同的编译器上, 下面的例程可能并不能获得与文中所述一致的结果.
    不过, 这篇文章的重点并非讲述 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;
}
问: 输出多少? A. 10 B. 20 C. 40 D. 以上答案都不正确.
    嗯, 也许有得到 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;
}
上一场中选 B 和 C 的同学也许会收到几枚链接错误报告, 而得到 A 的同学当然毫无压力.
    怎么样, 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++ 委员会还是秉承一贯的唯恐天下不乱的事态在制造古怪的新颖词法语法). 在转移构造函数中除了为被构造的对象指派上合适的值, 还要手动 "清理" 掉原有对象的成员, 避免原有对象析构时把指针给干掉了.

    另外, 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;
}

Post tags:   C++  C++11  Move Semantic  Copy Constructor

Leave a comment:




Creative Commons License Your comment will be licensed under
CC-NC-ND 3.0


. Back to Bit Focus
NijiPress - Copyright (C) Neuron Teckid @ Bit Focus
About this site