Copy Semantics in C++

Last Updated: 2023-12-28 03:49:34 Thursday

-- TOC --

在代码中,copy很常见,将一个变量的值赋给另一个变量,函数调用的参数和返回值传递,这些代码都涉及copy动作。在C代码中,这些copy动作的处理是很直接的,因为所有的C变量都代表了某一块内存,编译器直接做内存copy即可。但C++就不同了,如果把一个对象赋值给另一个变量(变量就是有名称的对象),如果不加以控制,容易出现shallow copy的错误。虽然C语言也存在shallow copy的问题,但C语言没有提供直接解决的机制,而是需要程序员自己绕过去。C++提供了直接解决shallow copy的机制,这就是copy semantics,由它实现deep copy

Copy Constructor

overload one constructor function,入参是对象自己的const reference,在这个constructor中,程序员自己实现一个完整的对象数据copy。

#include <cstdio>
#include <cstring>
#include <stdexcept>


struct xyz {
    xyz() = default;

    xyz(size_t size):
        tlen{},
        p{} {
        reset(size);
    }

    // copy constructor
    xyz(const xyz& a):
        tlen{},
        p{} {
        reset(a.get_tlen());
        content(a.get_p());
    }

    void resize(size_t size) {
        reset(size);
    }

    void content(const char *cont) {
        if (strlen(cont)+1 > tlen)
            throw std::length_error{"content string length is too long"};
        memset(p, 0, tlen);
        strncpy(p, cont, strlen(cont));
    }

    size_t get_tlen() const {  // const is needed
        return tlen;
    }

    char* get_p() const {  // const is needed
        return p;
    }

    void show() {
        printf("%s\n", p?p:"(nullptr)");
    }

    ~xyz() {
        delete[] p;
    }

private:
    void reset(size_t size) {
        delete[] p;
        if (size != 0)
            p = new char[size]{};
        else
            p = nullptr;
        tlen = size;
    }

    size_t tlen;
    char* p;
};


int main(void) {
    char cont[] = "1234567890";

    xyz x{};
    x.show();
    x.resize(8);
    try {
        x.content(cont);
        x.show();
    } catch(const std::length_error& e) {
        printf("except: %s, %s\n", cont, e.what());
    }

    x.resize(11);
    x.content("1234567890");
    x.show();

    // call copy constructor
    xyz y{x};
    y.show();

    cont[0] = 'a';
    cont[1] = 'b';
    y.resize(16);
    try {
        y.content(cont);
    } catch(const std::length_error& e) {
        printf("except: %s, %s\n", cont, e.what());
    }

    x.show();
    y.show();

    // call copy constructor
    xyz z = y;
    z.show();

    return 0;
}

运行效果如下:

$ g++ -Wall -Wextra tcc.cpp -o tcc
$ ./tcc
(nullptr)
except: 1234567890, content string length is too long
1234567890
1234567890
1234567890
ab34567890
ab34567890

上面的测试代码,y和z都是在调用copy constructor。虽然z的赋值用的是=,但这是在创建新对象的时候!

测试发现入参的const申明去掉后,编译没问题,在copy constructor中修改入参也可以。这说明本文所介绍的内容,应该只是一种best practice。

Copy Assignment

在非申明对象的时候,也可以用=号来实现deep copy,这就是copy assignment。实现copy assignment,需要程序员重载运算符,自己实现其代码逻辑。

增加copy assignment接口,重写main后,代码如下:

#include <cstdio>
#include <cstring>
#include <stdexcept>


struct xyz {
    xyz() = default;

    xyz(size_t size):
        tlen{},
        p{nullptr} {
        reset(size);
    }

    // copy constructor
    xyz(const xyz& a):
        tlen{},
        p{nullptr} {
        reset(a.get_tlen());
        content(a.get_p());
    }

    // copy assignment
    xyz& operator=(const xyz& a) {
        if (this == &a)
            return *this;
        reset(a.get_tlen());
        content(a.get_p());
        return *this;
    }

    void resize(size_t size) {
        reset(size);
    }

    void content(const char *cont) {
        if (strlen(cont)+1 > tlen)
            throw std::length_error{"content string length is too long"};
        memset(p, 0, tlen);
        strncpy(p, cont, strlen(cont));
    }

    size_t get_tlen() const {  // const is needed
        return tlen;
    }

    char* get_p() const {  // const is needed
        return p;
    }

    void show() {
        printf("%s\n", p?p:"(nullptr)");
    }

    ~xyz() {
        delete[] p;
    }

private:
    void reset(size_t size) {
        delete[] p;
        if (size != 0)
            p = new char[size]{};
        else
            p = nullptr;
        tlen = size;
    }

    size_t tlen;
    char* p;
};


int main(void) {
    // call copy constructor
    xyz y{};
    y.show();

    y.resize(16);
    y.content("123abc");
    y.show();

    // call copy assignment
    xyz z{};
    z = y;
    z.show();

    return 0;
}

运行效果如下:

$ g++ -Wall -Wextra tcc.cpp -o tcc
$ ./tcc
(nullptr)
123abc
123abc

注意:对象reference类型接口的返回值*this,我觉得可以理解为一种比较特殊的cast,即cast to ref!

上面这段测试代码,使用了default zero-parameter constructor,这意味着,如果创建对象的时候,不使用{}将成员变量清零,以上代码就会出现segmentation fault。因为copy assignment会先调用private的reset接口,一开始就delete[] p,此时p如果不是0,程序就挂了。

Default Copy

这篇文章的目的,就是说明C++中解决shallow copy的机制,copy semantics。如果对象拥有非局部的动态内存资源,基本上在需要copy的时候,都要程序员自己实现上面介绍的copy constructor和copy assignment。

而默认情况,default copy,对应的是兼容传统C语言的机制,调用所有member的copy接口!default copy是编译器自动提供的,程序员可以不用写出来。如果要写出来,就是如下面这样的代码:

struct Replicant {
    Replicant(const Replicant&) = default;
    Replicant& operator=(const Replicant&) = default;
    --snip--
};

Often, the compiler will generate default implementations for copy construction and copy assignment. The default implementation is to invoke copy construction or copy assignment on each of a class’s members. Any time a class manages a resource, you must be extremely careful with default copy semantics; they’re likely to be wrong. Best practice dictates that you explicitly declare that default copy assignment and copy construction are acceptable for such classes using the default keyword.

Delete Behavior

C++的语法支持定义不允许copy的对象,如果代码明确了不允许copy,又出现了copy,会有编译错误!

不允许对象copy的代码如下面这样:

struct Highlander {
    Highlander(const Highlander&) = delete;
    Highlander& operator=(const Highlander&) = delete;
    --snip--
};

Some classes simply cannot or should not be copied, for example, if your class manages a file or if it represents a mutual exclusion lock for concurrent programming. You can suppress the compiler from generating a copy constructor and a copy assignment operator using the delete keyword.

发现C++的一个秘密,似乎任何函数,都可以申明delete,代码中就不可再调用了。

#include <iostream>
using namespace std;


struct xyz {
    xyz(void) { cout << "xyz constructor\n"; }
    xyz(const xyz *x) = delete;
    xyz(int a) = delete;
    void show(int a) = delete;
    ~xyz(void) { cout << "xyz destructor\n"; }
};


void func(void) = delete;


int main(void) {
    //xyz x{1};
    //x.show(1);
    func();
    return 0;
}

Pointer Copy

这个词是我发明的,表示一个传入相同对象类型的指针的copy constructor或copy assignement。

#include <iostream>
using namespace std;


struct xyz {
    xyz(void) { cout << "xyz constructor\n"; }
    xyz(const xyz *x) { cout << "xyz pointer copy constructor\n"; }
    ~xyz(void) { cout << "xyz destructor\n"; }

    xyz& operator=(const xyz *x) {
        if (this == x)
            return *this;
        cout << "xyz pointer assign constructor\n";
        return *this;
    }
};


int main(void) {
    xyz x{};
    xyz y{&x};
    xyz z;
    z = &x;
    z = &z;
    return 0;
}

输出:

xyz constructor
xyz pointer copy constructor
xyz constructor
xyz pointer assign constructor
xyz destructor
xyz destructor
xyz destructor

pointer copy本质上就是constructor的overload。上面的代码看起来稍微有一点怪异,一般不会这样用。但std::allocator容器中的对象,可能需要支持这样的接口。

本文链接:https://cs.pynote.net/sf/c/cpp/202208301/

-- EOF --

-- MORE --