代码之家  ›  专栏  ›  技术社区  ›  gct

复制构造函数优先于移动构造函数?[副本]

  •  1
  • gct  · 技术社区  · 7 年前

    这个问题已经有了答案:

    为了好玩,我正在开发一个简单的json解析器,我得到了我的值类型:

    typedef enum {
        JSON_NULL,
        JSON_NUMBER,
        JSON_STRING,
        JSON_ARRAY,
        JSON_OBJECT,
        JSON_BOOLEAN
    } json_type_t;
    
    // datatype for json value
    struct json_value {
        using arr_type = vector<json_value>;
        using obj_pair = pair<string, json_value>;
        using obj_type = unordered_map<string, json_value>;
    
        // constructors 
        json_value()
            : type(JSON_NULL) {}
    
        json_value(json_type_t type)
            : type(type) {
            switch(type) {
                case JSON_STRING:  str = new string;   break;
                case JSON_ARRAY:   arr = new arr_type; break;
                case JSON_OBJECT:  obj = new obj_type; break;
                default:
                    break;
            }
        }
    
        // copy construct
        json_value(const json_value& other) {
            printf("copying json value\n");
            if (other.type != JSON_NULL) {
                type = other.type;
                switch(type) {
                    case JSON_NULL:                                    return;
                    case JSON_NUMBER:  num = other.num;                return;
                    case JSON_BOOLEAN: val = other.val;                return;         
                    case JSON_STRING:  str = new string  (*other.str); return;
                    case JSON_ARRAY:   arr = new arr_type(*other.arr); return;
                    case JSON_OBJECT:  obj = new obj_type(*other.obj); return;
                }
            }
        }
    
        // move construct
        json_value(json_value&& other) {
            type = other.type;
            switch(type) {
                case JSON_NULL:                     break;
                case JSON_NUMBER:  num = other.num; break;
                case JSON_BOOLEAN: val = other.val; break; 
                case JSON_STRING:  str = other.str; other.str = nullptr; break;
                case JSON_ARRAY:   arr = other.arr; other.arr = nullptr; break;
                case JSON_OBJECT:  obj = other.obj; other.obj = nullptr; break;
            }
        }
    
        // assignment operator copy/swap idiom
        json_value& operator =(json_value other) {
            destroy();
            type = other.type;
            switch(type) {
                case JSON_NULL:                     break;
                case JSON_NUMBER:  num = other.num; break;
                case JSON_BOOLEAN: val = other.val; break; 
                case JSON_STRING:  str = other.str; other.str = nullptr; break;
                case JSON_ARRAY:   arr = other.arr; other.arr = nullptr; break;
                case JSON_OBJECT:  obj = other.obj; other.obj = nullptr; break;
            }
            return *this;
        }
    
        // destructor
        ~json_value() {
            destroy();
        }
    
        // type of value and union to hold data
        json_type_t type = JSON_NULL;
        union { 
            bool      val;
            double    num;
            string   *str;
            arr_type *arr;
            obj_type *obj;
        };
    
    private:
        // cleanup our memory
        void destroy() { 
            switch(type) {
                case JSON_NULL:    break;
                case JSON_NUMBER:  break;
                case JSON_BOOLEAN: break; 
                case JSON_STRING:  delete str; break;
                case JSON_ARRAY:   delete arr; break;
                case JSON_OBJECT:  delete obj; break;
            }
            type = JSON_NULL;
        }
    };
    

    我已经编写了正确的复制/移动构造函数和赋值运算符。我的问题是,当运行一个特定的基准测试时,解析器需要大约40毫秒的时间,为了进行一些优化,我注释掉了copy构造函数,以确保我没有生成任何不必要的副本。当然,我的代码仍然在编译,这表明move构造函数已经足够了, 快25%!

    检测复制构造函数,我可以看到它确实被调用了,但是正如我所示,move构造函数已经足够了。

    所以,我的问题是,在什么情况下,复制构造函数比移动构造函数更受欢迎,我如何才能找到发生这种情况的地方?

    1 回复  |  直到 7 年前
        1
  •  2
  •   Justin    6 年前

    标准容器都试图拥有 strong exception guarantee ,也就是说,如果抛出异常,就好像什么都没发生。

    std::vector 是的。为了保持这种保证,它只能在保证moving不会抛出的情况下使用move构造函数。考虑向量需要调整其缓冲区大小的情况:

    d // new element to push_back
    [a][b][c] // old, filled buffer
    [ ][ ][ ][ ][ ][ ] // new, empty buffer
    

    将新元素移动到位并不是问题,即使它抛出了,因为我们仍然可以使用旧的缓冲区:

    [a][b][c]
    [ ][ ][ ][d][ ][ ]
    

    但是,当我们将旧缓冲区的元素移到新缓冲区中时,如果我们在中间抛出会发生什么?

    [ ][#][c]
    [a][#][ ][d][ ][ ]
    

    我们不知道 b 当它抛出的时候,我们如何重建旧的状态?即使我们能复活 ,我们不能只移动先前的元素,因为移动它们也可能会抛出。

    如果我们回到副本上,我们可以随时放弃新的缓冲区。

    因此,为了保持 强有力的例外保证 我是说, STD::载体 除非move构造函数是 noexcept 是的。


    将移动操作声明为 无例外 是标准容器使用它们所必需的。大多数情况下,move构造函数和move赋值可以是 无例外 ,因此当它们是:

    json_value(json_value&& other) noexcept {
        // ...
    }
    
    json_value& operator=(json_value&& other) noexcept {
        // ...
    }