现代C++新特性 默认和删除函数

《现代C++新特性 默认和删除函数》

   《现代C++新特性 默认和删除函数》文字版PDF文档链接:现代C++新特性(文字版)-C++文档类资源-CSDN下载 

​​​​​​​1.类的特殊成员函数

在定义一个类的时候,我们可能会省略类的构造函数,因为C++标准规定,在没有自定义构造函数的情况下,编译器会为类添加默认的构造函数。像这样有特殊待遇的成员函数一共有6个(C++11以前是4 个),具体如下。

1.默认构造函数。

2.析构函数。

3.复制构造函数。

4.复制赋值运算符函数。

5.移动构造函数(C++11新增)。

6.移动赋值运算符函数(C++11新增)。

添加默认特殊成员函数的这条特性非常实用,它让程序员可以有更多精力关注类本身的功能而不必为了某些语法特性而分心,同时也避免了让程序员编写重复的代码,比如:

#include <string> 
#include <vector> 

class City {
    string name;
    vector<string> street_name;
};

int main(int argc, char** argv)
{
    City a;
    City b;
    a = b;
    return 0;
}

在上面的代码中,我们虽然没有为City类添加复制赋值运算符函数City:: operator= (const City &),但是编译器仍然可以成功编译代码,并且在运行过程中正确地调用string和vector<string>的复制赋值运算符函数。假如编译器没有提供这条特性,我们就不得不在编写类的时候添加以下代码:

City& City::operator=(const City& other)
{
    name = other.name;
    street_name = other.street_name;
    return *this;
}

很明显,编写这段代码除了满足语法的需求以外没有其他意义,很庆幸可以把这件事情交给编译器去处理。不过还不能高兴得太早,因为该特性的存在也给我们带来了一些麻烦。

1.声明任何构造函数都会抑制默认构造函数的添加。

2.一旦用自定义构造函数代替默认构造函数,类就将转变为非平凡类型。

3.没有明确的办法彻底禁止特殊成员函数的生成(C++11之前)。

下面来详细地解析这些问题,还是以City类为例,我们给它添加一个构造函数:

#include <string> 
#include <vector> 

class City {
    string name;
    vector<string> street_name;
public:
    City(const char* n) : name(n) {}
};

int main(int argc, char** argv)
{
    City a("wuhan");
    City b;  // 编译失败,自定义构造函数抑制了默认构造函数  
    b = a;
    return 0;
}

以上代码由于添加了构造函数City(const char *n),导致编译器不再为类提供默认构造函数,因此在声明对象b的时候出现编译错误,为了解决这个问题我们不得不添加一个无参数的构造函数:

class City {
    string name;
    vector<string> street_name;
public:
    City(const char* n) : name(n) {}
    City() {}    // 新添加的构造函数 
};

可以看到这段代码新添加的构造函数什么也没做,但却必须定义。乍看虽然做了一些多此一举的工作,但是毕竟也能让程序重新编译和运行,问题得到了解决。真的是这样吗?事实上,我们又不知不觉地陷入另一个麻烦中,请看下面的代码:

class Trivial {
    int i;
public:
    Trivial(int n) : i(n), j(n) {}
    Trivial() {}
    int j;
};

int main(int argc, char** argv)
{
    Trivial a(5);
    Trivial b;
    b = a;
    cout << "is_trivial_v<Trivial> : " << is_trivial_v<Trivial> << endl;
    return 0;
}

上面的代码中有两个动作会将Trivial类的类型从一个平凡类型转变为非平凡类型。第一是定义了一个构造函数Trivial(int n),它导致编译器抑制添加默认构造函数,于是Trivial类转变为非平凡类型。第二是定义了一个无参数的构造函数,同样可以让Trivial类转变为非平凡类型。

后一个问题大家肯定也都遇到过,举例来说,有时候我们需要编写一个禁止复制操作的类,但是过去C++标准并没有提供这样的能力。聪明的程序员通过将复制构造函数和复制赋值运算符函数声明为 private并且不提供函数实现的方式,间接地达成目的。为了使用方便,boost库也提供了noncopyable类辅助我们完成禁止复制的需求。

不过就如前面的问题一样,虽然能间接地完成禁止复制的需求,但是这样的实现方法并不完美。比如,友元就能够在编译阶段破坏类对复制的禁止。这里可能会有读者反驳,虽然友元能够访问私有的复制构造函数,但是别忘了,我们并没有实现这个函数,也就是说程序后仍然无法运行。没错,程序 后会在链接阶段报错,原因是找不到复制构造函数的实现。但是这个报错显然来得有些晚,试想一下,如果面临的是一个巨大的项目,有不计其数的源文件需要编译,那么编译过程将非常耗时。如果某个错误需要等到编译结束以后的链接阶段才能确定,那么修改错误的时间代价将会非常高,所以我们还是更希望能在编译阶段就找到错误。

还有一个典型的例子,禁止重载函数的某些版本,考虑下面的例子:

class Base {
    void foo(long&);
public:
    void foo(int) {}
};

int main(int argc, char** argv) 
{
    Base b;
    long l = 5;
    b.foo(8);
    b.foo(l);       // 编译错误
    return 0;
}

由于将成员函数foo(long &)声明为私有访问并且没有提供代码实现,因此在调用b.foo(l)的时候会编译出错。这样看来它跟我们之前讨论的例子没有什么实际区别,再进一步讨论,假设现在我们需要继承Base类,并且实现子类的foo函数;另外,还想沿用基类Base的foo函数,于是这里使用using说明符将Base的foo成员函数引入子类,代码如下:

class Base {
    void foo(long&);
public:
    void foo(int) {}
};

class Derived : public Base {
public:
    using Base::foo;
    void foo(const char*) {}
};

int main(int argc, char** argv)
{
    Derived d;
    d.foo("hello");
    d.foo(5);
    return 0;
}

上面这段代码看上去合情合理,而实际上却无法通过编译。因为 using说明符无法将基类的私有成员函数引入子类当中,即使这里我们将代码d.foo(5)删除,即不再调用基类的函数,编译器也是不会让这段代码编译成功的。

​​​​​​​2.显式默认和显式删除

为了解决以上种种问题,C++11标准提供了一种方法能够简单有效又精确地控制默认特殊成员函数的添加和删除,我们将这种方法叫作显式默认和显式删除。显式默认和显式删除的语法非常简单,只需要在声明函数的尾部添加=default和=delete,它们分别指示编译器添加特殊函数的默认版本以及删除指定的函数:

struct type {
    type() = default;  
    virtual ~type() = delete;  
    type(const type&);
};

type::type(const type&) = default;

以上代码显式地添加了默认构造和复制构造函数,同时也删除了析构函数。请注意,=default可以添加到类内部函数声明,也可以添加到类外部。这里默认构造函数的=default就是添加在类内部,而复制构造函数的=default则是添加在类外部。提供这种能力的意义在于,它可以让我们在不修改头文件里函数声明的情况下,改变函数内部的行为,例如:

// type.h 
struct type {
    type();
    int x;
};

// type1.cpp 
type::type() = default;

// type2.cpp 
type::type()
{
    x = 3;
}

=delete与=default不同,它必须添加在类内部的函数声明中,如果将其添加到类外部,那么会引发编译错误。通过使用=default,我们可以很容易地解决之前提到的前两个问题,请观察以下代码:

class NonTrivial {
    int i;
public:
    NonTrivial(int n) : i(n), j(n) {}
    NonTrivial() {}
    int j;
};

class Trivial {
    int i;
public:
    Trivial(int n) : i(n), j(n) {}
    Trivial() = default;
    int j;
};

int main(int argc, char** argv)
{
    Trivial a(5);
    Trivial b;
    b = a;
    cout << "is_trivial_v<Trivial>    : " << is_trivial_v<Trivial> << endl;
    cout << "is_trivial_v<NonTrivial> : " << is_trivial_v<NonTrivial> << endl;
    return 0;
}

注意,我们只是将构造函数NonTrivial() {}替换为显式默认构造函数Trivial() = default,类就从非平凡类型恢复到平凡类型了。这样一来,既让编译器为类提供了默认构造函数,又保持了类本身的性质,可以说完美解决了之前的问题。

另外,针对禁止调用某些函数的问题,我们可以使用= delete来删除特定函数,相对于使用private限制函数访问,使用= delete更加彻底,它从编译层面上抑制了函数的生成,所以无论调用者是什么身份(包括类的成员函数),都无法调用被删除的函数。进一步来说,由于必须在函数声明中使用= delete来删除函数,因此编译器可以在第一时间发现有代码错误地调用被删除的函数并且显示错误报告,这种快速报告错误的能力也是我们需要的,来看下面的代码:

class NonCopyable {
public:
    NonCopyable() = default;                              // 显式添加默认构造函数 
    NonCopyable(const NonCopyable&) = delete;             // 显式删除复制构造函数 
    NonCopyable& operator=(const NonCopyable&) = delete;  // 显式删除复制赋值运算符函数 
};
int main(int argc, char** argv)
{
    NonCopyable a;
    NonCopyable b;
    a = b;              //编译失败,复制赋值运算符已被删除 
    return 0;
}

以上代码删除了类NonCopyable的复制构造函数和复制赋值运算符函数,这样就禁止了该类对象相互之间的复制操作。请注意,由于显式地删除了复制构造函数,导致默认情况下编译器也不再自动添加默认构造函数,因此我们必须显式地让编译器添加默认构造函数,否则会导致编译失败。

然后,让我们用= delete来解决禁止重载函数的继承问题,这里只需要对基类Base稍作修改即可:

class Base {
    // void foo(long &);
public:
    void foo(long&) = delete;    // 删除foo(long &)函数  
    void foo(int) {} 
};

class Derived : public Base {
public:
    using Base::foo;
    void foo(const char*) {}
};

int main(int argc, char** argv)
{
    Derived d;
    d.foo("hello");
    d.foo(5);
    return 0;
    return 0;
}

请注意,上面对代码做了两处修改。第一是将foo(long &)函数从private移动到public,第二是显式删除该函数。如果只是显式删除了函数,却没有将函数移动到public,那么编译还是会出错的。

​​​​​​​3.显式删除的其他用法

显式删除不仅适用于类的成员函数,对于普通函数同样有效。只不过相对于应用于成员函数,应用于普通函数的意义就不大了:

void foo() = delete;
static void bar() = delete; 

int main(int argc, char** argv)
{
    bar();        // 编译失败,函数已经被显式删除   
    foo();        // 编译失败,函数已经被显式删除 
    return 0;
}

另外,显式删除还可以用于类的new运算符和类析构函数。显式删除特定类的new运算符可以阻止该类在堆上动态创建对象,换句话说它可以限制类的使用者只能通过自动变量、静态变量或者全局变量的方式创建对象,例如:

struct type {
    void* operator new(size_t) = delete;
};
type global_var;

int main(int argc, char** argv)
{
    static type static_var;
    type auto_var;
    type* var_ptr = new type;    // 编译失败,该类的new已被删除 
    return 0;
}

显式删除类的析构函数在某种程度上和删除new运算符的目的正好相反,它阻止类通过自动变量、静态变量或者全局变量的方式创建对象,但是却可以通过new运算符创建对象。原因是删除析构函数后,类无法进行析构。所以像自动变量、静态变量或者全局变量这种会隐式调用析构函数的对象就无法创建了,当然了,通过new运算符创建的对象也无法通过delete销毁,例如:

struct type {
    ~type() = delete;
}; 

type global_var;                 // 编译失败,析构函数被删除无法隐式调用 

int main(int argc, char** argv)
{
    static type static_var;      // 编译失败,析构函数被删除无法隐式调用   
    type auto_var;               // 编译失败,析构函数被删除无法隐式调用 
    type* var_ptr = new type;
    delete var_ptr;              // 编译失败,析构函数被删除无法显式调用 
    return 0;
}

通过上面的代码可以看出,只有new创建对象会成功,其他创建和销毁操作都会失败,所以这样的用法并不多见,大部分情况可能在单例模式中出现。

​​​​​​​4.explicit和=delete

在类的构造函数上同时使用explicit和=delete是一个不明智的做法,它常常会造成代码行为混乱难以理解,应尽量避免这样做。下面这个例子就是反面教材:

struct type {
    type(long long) {}
    explicit type(long) = delete;
};
void foo(type) {}

int main(int argc, char** argv)
{
    foo(type(58));  
    foo(58);
    return 0;
}

读者可以在这里思考一下,上面哪句代码无法通过编译。答案是 foo(type(58))会造成编译失败,原因是type(58)显式调用了构造函数,但是explicit type(long)却被删除了。foo(58)可以通过编译,因为编译器会选择type(long long)来构造对象。虽然原因解释得很清楚,但是建议还是不要这么使用,因为这样除了让人难以理解外,没有实际作用。

    原文作者:神奇的小强
    原文地址: https://blog.csdn.net/qq_26405165/article/details/123908010
    本文转自网络文章,转载此文章仅为分享知识,如有侵权,请联系博主进行删除。
点赞