用explicit申明constructor

Last Updated: 2023-08-20 14:15:49 Sunday

-- TOC --

explicit申明对constructor的作用,就是限制implicit转换。比如在用=给一个对象赋值的时候,或函数调用传参的时候,都可能出现的隐式的对象创建(类型转换)。

请看下面这段测试代码,关注class xyz在main中是如何使用的:

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


class xyz {
public:
    size_t tlen;
    char *p;

    xyz():
        tlen{},
        p{} {
    }

    xyz(size_t size) {
        printf("size_t constructor\n");
        if (size != 0)
            p = new char[size]{};
        else
            p = NULL;
        tlen = size;
    }

    xyz(const char *cont) {
        printf("const char constructor\n");
        if (cont != NULL) {
            tlen = strlen(cont) + 1;
            p = new char[tlen];
            strcpy(p, cont);
        } else {
            p = NULL;
            tlen = 0;
        }
    }

    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));
    }

    void show() {
        if (p)
            printf("%s\n", p);
        else
            printf("%s\n", "(NULL)");
    }

    ~xyz() {
        delete[] p;
    }
};


int main(void) {
    char cont[] = "1234567890";
    class xyz x = 24;  // what?
    x.content(cont);
    x.show();

    class xyz y = "abcdefg";  // what?
    y.show();

    return 0;
}

在main中,创建了两个xyz对象,他们的初始化,使用了=符号!

这是合法的,我在C++花式对象初始化这篇文章中,总结过C++支持的各种对象初始化的方法,使用=是兼容C风格的语法,也比较符合很多程序员的直觉。

C语言中的对象都是所谓的primitive type,这些对象只有一块内存存储数据,没有member functions,初始化就是简单直接地赋值,初始化struct也是赋值。而C++的对象,初始化需要调用constructor,这就让问题复杂了一点点。C++支持函数重载,即相同名称的函数接口,可以有不同的参数,由编译器区分调用时具体指向哪个接口。constructor也可以重载,因此就会出现单参数的constructor,而C++语法,支持用=的方式,调用单参数constructor。这就是上面测试代码的逻辑。

上面测试代码,打印输出:

$ g++ -Wall -Wextra test_cc.cpp -o test_cc
$ ./test_cc
size_t constructor
1234567890
const char constructor
abcdefg

似乎一切都没问题,但这种语法的存在,容易让程序员写出下面这样的错误代码:

int main(void) {
    class xyz z;
    z.show();
    printf("%zu\n", z.tlen);

    z = 32;
    z.show();
    printf("%zu\n", z.tlen);
    return 0;
}

先创建z对象,此时会调用无参数的constructor。然后尝试执行z=32!错误就出现在这里!先看看运行时会发生什么样的严重错误:

$ g++ -Wall -Wextra test_cc.cpp -o test_cc
$ ./test_cc
(NULL)
0
size_t constructor
|
32
free(): double free detected in tcache 2
Aborted (core dumped)

double free,为什么会出现double free这样严重的错误?

因为,z=32这行代码的执行逻辑是这样的:

{
    class xyz temp = 32;  // implicit type conversion
    z = temp;
}

编译器会创建一个临时的对象temp,然后使用copy assignment赋值给z对象(=号两边类型不同,必须这么做,这是C++的lenient rule)。代码应该会先释放temp对象(我猜的,因此上面的代码,我用了大括号括起来),在打印出32后,程序结束,再释放z对象。而编译器提供的默认copy assignment只能完成shallow copy,因此导致了对象内指针p的double free。

推荐学习:详解C++的copy assignment

解决问题的方法有二:

  1. 自己定义一个copy assignment,默认的不能用;
  2. 使用explicit申明constructor;

方法1的代码如下:

    class xyz& operator=(const class xyz& a) {
        if (this == &a)
            return *this;
        if (p)
            delete[] p;
        tlen = a.tlen;
        p = new char[tlen];
        strcpy(p, a.p);
        return *this;
    }

方法1可以解决double free的错误。

方法2,就是本文的主题,使用explicit申明单参数constructor,将不允许出现上述的隐式的类型转换。

推荐使用方法2,让一切都变得更简单,减少C++代码的歧义。

按照方法2,上面的测试代码,两个单参数constructor修改为:

    explicit xyz(size_t size) {
        printf("size_t constructor\n");
        if (size != 0)
            p = new char[size]{};
        else
            p = NULL;
        tlen = size;
    }

    explicit xyz(const char *cont) {
        printf("const char constructor\n");
        if (cont != NULL) {
            tlen = strlen(cont) + 1;
            p = new char[tlen];
            strcpy(p, cont);
        } else {
            p = NULL;
            tlen = 0;
        }
    }

记得C++推荐用花括号做对象初始化。

还有一种情况,也建议考虑使用explicit申明。

C++支持给函数入参提供默认值,因此,有默认值的constructor接口,也可能形成单参数接口。请看下面的测试代码:

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


class xyz {
public:
    size_t tlen;
    char *p;

    explicit xyz(size_t size, const char *cont="1234") {
        if (size > strlen(cont)) {
            p = new char[size]{};
            strncpy(p, cont, strlen(cont));
        }
        else
            throw std::length_error{"size is le strlen(cont)"};
        tlen = 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));
    }

    void show() {
        if (p)
            printf("%s\n", p);
        else
            printf("%s\n", "(NULL)");
    }

    ~xyz() {
        if (p)
            delete[] p;
    }
};


int main(void) {
    //xyz x = 7; // wrong because of explicit
    xyz y{8};
    y.show();

    xyz z{9,"abcdefg"};
    z.show();

    return 0;
}

因为等号只能带一个参数,所以这种语法会让编译器去尝试调用只需要一个参数的接口。而使用explicit,彻底限制了这样的操作。这很好,C++就应该有C++的样儿!

传参时发生的implicit conversion

#include <iostream>
using namespace std;

struct you{
    int a;
    int b;
    you(int a):a{a}{}
};

void func(you y){
    cout << y.a << y.b << endl;
}

int main(void) {
    func(1);
    return 0;
}

func只有一个参数,是个class对象,调用时使用了一个常量,此时常量会隐式地转为对象。

解决方法:

所有constructor都应该explicit吗?

下面代码是合法的:

#include <iostream>
using namespace std;

struct you{
    int a;
    int b;
    int c;
    you(int a, int b, int c): a{a},b{b},c{c}{}
};

void func(you y){
    cout << y.a << y.b << y.c << endl;
}

int main(void) {
    func({1,2,3});
    return 0;
}

如果给you唯一的constructor接口增加explicit申明,编译错误!

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

-- EOF --

-- MORE --