返回

[Effective C++ 笔记]06.明确拒绝你不想使用的编译器自动生成的函数

简介

本条款推荐一些方法使得我们可以拒绝编译器为我们生成我们不需要的函数,简而言之可以概括为:

  • 将不需要编译器自动生成的函数声明为 private
  • 将不需要编译器自动生成的函数在基类中声明为 private
  • C++11 之后可以将不需要编译器生成的函数声明并标记为 delete

引子

根据 条款 05 所述,当我们没有显式声明 构造函数、析构函数、copy 构造函数和 copy assignment 操作符时,编译器会自动为我们生成这些函数。但是在某些情况下,我们可能并不想它为我们生成这些函数。考虑以下例子(具体代码参考 Ex1):

class HomeForSale {
    // ...
};

上述类描述了一个代售房屋,通常情况下每个房屋都是独一无二的,所以我们不希望这个类具有拷贝行为,因此下面的代码应该不合法:

HomeForSale h1;
HomeForSale h2;
HomeForSale h3(h1);     // 不应该成功
h2 = h1;                // 不应该成功

虽然我们没有声明 copy 构造函数和 copy assignment 操作符,但是因为编译器自动帮我们生成了这个两个函数,所以编译和运行都能通过。那么我们有什么办法可以组织编译器的这种行为呢?下面介绍几种思路:

将不需要编译器自动生成的函数声明为 private

首先,我们之可以在 class 外部直接调用编译器生成的函数是因为其生成的函数都是 public 的,因此为了阻止外部使用 copy 构造函数和 copy assignment 操作符,我们可以在类中显式地将其声明为 private,如下所示(具体代码参考 Ex2):

class HomeForSale {
public:
    HomeForSale() {}
private:
    HomeForSale(const HomeForSale&) {}
    HomeForSale& operator=(const HomeForSale&) {}
};

这样当用户企图在 class 外部调用函数时编译会不通过。调用时会报错:

HomeForSale h1;
HomeForSale h2;
HomeForSale h3(h2); // compiles fails
h2 = h1;            // compiles fails

报错信息:

main.cpp: In function ‘int main()’:
main.cpp:17:22: error: ‘HomeForSale::HomeForSale(const HomeForSale&)’ is private within this context
     HomeForSale h3(h2); // compiles and runs without error, which it shouldn't
                      ^
main.cpp:9:5: note: declared private here
     HomeForSale(const HomeForSale&);
     ^~~~~~~~~~~
main.cpp:18:10: error: ‘HomeForSale& HomeForSale::operator=(const HomeForSale&)’ is private within this context
     h2 = h1;            // compiles and runs without error, which it shouldn't
          ^~
main.cpp:10:18: note: declared private here
     HomeForSale& operator=(const HomeForSale&);

这种方法并不是绝对安全的,虽然将其声明为 private,但是在类内部的 member function 和 friend 类中还是有可能调用的,如下所示(具体代码参考 Ex3):

class HomeForSale {
public:
    HomeForSale() {}

    void test() {

        HomeForSale h1;
        HomeForSale h2 = h1;
    }

private:
    HomeForSale(const HomeForSale&) {}
    HomeForSale& operator=(const HomeForSale&) {}
};

HomeForSale h1;
h1.test();      // compiles & runs without error, which it shouldn't

这种情况下我们没有办法阻止其编译,但是我们可以通过不提供实现的方式,使它在链接期报错,参考以下例子(具体代码参考 Ex4):

class HomeForSale {
public:
    HomeForSale() {}

    void test() {

        HomeForSale h1;
        HomeForSale h2 = h1;
    }

private:
    HomeForSale(const HomeForSale&);
    HomeForSale& operator=(const HomeForSale&);
};

HomeForSale h1;
h1.test(); // linking fails

上述代码会报以下链接错误:

/tmp/ccuFJ2sv.o: In function `HomeForSale::test()':
main.cpp:(.text._ZN11HomeForSale4testEv[_ZN11HomeForSale4testEv]+0x36): undefined reference to `HomeForSale::HomeForSale(HomeForSale const&)'
collect2: error: ld returned 1 exit status

总的来说,通过将我们不希望编译器生成的函数声明为 private 并不提供实现,可以保证类外部调用时编译器失败类内部以及friend类中调用时在链接期失败

将不需要编译器自动生成的函数在基类中声明为 private

当我们为了节省时间,希望尽量早检测出错误,有没有办法将上述方法中的链接期错误也提前至编译器呢?根据 条款 05 中提到编译器拒绝自动生成函数的条件,我们可以在基类中声明不需要的函数为 private,令我们要使用的类继承它。这样当我们想要调用 copy 构造函数时,编译器会尝试生成 copy 构造函数,但由于在基类中我们已将其声明为 private,编译器会拒绝生成并报错误。这样就达到了我们想要的效果,具体参考以下代码(具体代码参考 Ex5):

class Uncopyable {
public:
    Uncopyable() {}
private:
    Uncopyable(const Uncopyable&);
    Uncopyable& operator=(const Uncopyable&);
};

/**
 * this class should not be copyable
 */
class HomeForSale: public Uncopyable {
public:
    HomeForSale() {}

    void test() {

        HomeForSale h1;
        HomeForSale h2 = h1;
    }
};

上述代码中 HomeForSale::test() 会在编译器失败并报以下错误:

main.cpp: In member function ‘void HomeForSale::test()’:
main.cpp:20:26: error: use of deleted function ‘HomeForSale::HomeForSale(const HomeForSale&)’
         HomeForSale h2 = h1;
                          ^~
main.cpp:13:7: note: ‘HomeForSale::HomeForSale(const HomeForSale&)’ is implicitly deleted because the default definition would be ill-formed:
 class HomeForSale: public Uncopyable {
       ^~~~~~~~~~~
main.cpp:13:7: error: ‘Uncopyable::Uncopyable(const Uncopyable&)’ is private within this context
main.cpp:6:5: note: declared private here
     Uncopyable(const Uncopyable&);
     ^~~~~~~~~~

因此通过这种方法也可以达到我们想要的效果。

将不需要编译器生成的函数声明并标记为 delete

原书由于写作时间比较久远,所以没有提到这个方法。 c++11 中引入了一个新的特性使得我们可以显式的将我们不需要的函数标记为 delete,相对于上面两种方法而言更加简洁和直观。具体代码如下所示(具体代码参考 Ex6):

class HomeForSale {
public:
    HomeForSale() {}

    HomeForSale(const HomeForSale&) = delete;
    HomeForSale& operator=(const HomeForSale&) = delete;

    void test() {

        HomeForSale h1;
        HomeForSale h2 = h1;    // compile fails
        HomeForSale h3(h2);     // compile fails
    }
};

上述代码在编译器会编译失败,并报以下错误,因此也能满足我们的需求:

main.cpp: In member function ‘void HomeForSale::test()’:
main.cpp:15:26: error: use of deleted function ‘HomeForSale::HomeForSale(const HomeForSale&)’
         HomeForSale h2 = h1;
                          ^~
main.cpp:9:5: note: declared here
     HomeForSale(const HomeForSale&) = delete;
     ^~~~~~~~~~~
main.cpp:16:26: error: use of deleted function ‘HomeForSale::HomeForSale(const HomeForSale&)’
         HomeForSale h3(h2);
                          ^
main.cpp:9:5: note: declared here
     HomeForSale(const HomeForSale&) = delete;
     ^~~~~~~~~~~

结论

为了拒绝编译器自动为我们生成不需要的函数,可以通过以下方式:

  • 将不需要编译器自动生成的函数声明为 private
  • 将不需要编译器自动生成的函数在基类中声明为 private
  • C++11 之后可以将不需要编译器生成的函数声明并标记为 delete

完整可运行代码地址:Effective-Cpp-Reading-Note

Built with Hugo
Theme Stack designed by Jimmy