为什么要显式删除构造函数而不是将其设为私有?

129

什么时候/为什么我会想要显式删除我的构造函数?假设原因是为了防止其使用,为什么不只是将其设为private

class Foo
{ 
  public: 
    Foo() = delete; 
};

18
这段话的意思是:= default 和 “Use of deleted function” 搭配得很好,不仅类无法使用它,我个人也更喜欢看到“Use of deleted function”而不是“Function is private”。前者明确表明“这不应该被使用”,如果从中能得到任何东西,那么类无法使用它实际上会产生语义上的差异。 - chris
12
对我来说,由于我很少使用C++11,这篇文章对我来说比原作者意识到的还要有用。我甚至不知道您可以将构造函数标记为“删除”。无论是问题本身还是Luchian的答案都非常有建设性。任何不熟悉C++11细节但很快就需要了解的人都可以从中获益。 - WhozCraig
1
可能是删除默认类构造函数的意义是什么?的重复问题。 - user673679
4个回答

126

怎么样:

//deleted constructor
class Foo
{ 
  public: 
    Foo() = delete;     
  public:
    static void foo();
};

void Foo::foo()
{
   Foo f;    //illegal
}
对抗
//private constructor
class Foo
{ 
  private: 
    Foo() {}     
  public:
    static void foo();
};

void Foo::foo()
{
   Foo f;    //legal
}

它们基本上是不同的东西。 private 告诉你只有该类的成员(或者友元)才能调用该方法或访问该变量。在这种情况下,该类的静态方法(或任何其他成员)可以调用类的 private 构造函数是合法的。但对于删除的构造函数则不适用。

示例请参见此处


3
如果您声明了Foo(int),则根本不需要声明Foo()。Foo()将不会被生成,因此无论如何Foo f都是无效的。因此,您的示例并未展示删除构造函数的情况。请自行查看 - http://ideone.com/mogiIF - mark
1
@mark 我写了两个构造函数来证明这一点。我会编辑一下,让大家都能看懂。 - Luchian Grigore
1
我理解这个区别,只是不理解删除语句的总体和构造函数的特定增值。毕竟,我可以指定一个没有主体的私有默认构造函数。那么代码也会失败,只是在链接时。嗯,我可以看到删除更明确地传达了意图,但也仅此而已。 - mark
16
@mark 是的,那将是按 C++98 的方式做事。但在编程中清晰地传达意图实际上是非常重要的事情。在这种情况下,一些读者可能会看到一个私有未定义构造函数,并认为它是意外的,并且只是添加一个默认构造函数的定义,特别是如果定义像默认构造函数一样简单(是的,添加注释有帮助,但我们更喜欢编译器强制执行而不是注释强制执行)。通过使我们的意图更清晰,我们还可以获得更好的错误信息,而不是“未定义引用”的错误信息。 - mpark
2
我真的不明白这个回答如何解决主要问题。标题中的问题和帖子中OP的第一个问题是:我何时/为什么想要显式删除我的构造函数? - Alexander Bolinsky

27

为什么要显式删除构造函数?

另一个原因:

当我想确保一个类被调用时具有初始化器时,我会使用delete。我认为这是一种非常优雅的方式,在不进行运行时检查的情况下实现这一点。

C++编译器会为您执行此检查。

class Foo
{
   public:
       Foo() = delete;
       Foo(int bar) : m_bar(bar) {};
   private:
       int m_bar;
};

这段代码(非常简化)确保没有像这样的实例化(默认构造): Foo foo;

29
这里删除的声明是不必要的。它将随着任何用户提供的构造函数自动删除。 - Mike Lui
13
为了澄清@MikeLui的评论,被删除的声明对于编译器来说是不必要的。但在许多情况下,像这样的代码应该被包括以声明对其他程序员的意图。 - Jeff G
3
除了声明您的意图外,它还创建了一个明显的位置来记录您在公共接口中删除原因,并且编译器错误将是类似于“使用已删除的函数”的简短内容。如果 Foo 有许多构造函数,只是没有默认构造函数,那么 Foo foo; 将导致更长的错误列表,列出所有未能匹配的隐式定义、受保护和私有构造函数。 - sigma
1
我仍然不明白在构造函数声明中使用“= delete”关键字的额外行如何比没有默认构造函数更好地表达“无默认构造函数”的意图?例如:我不想在我的代码中声明变量“a”,写“// int a; // 不需要定义变量a”还是在代码中不写有关此变量的任何内容更好? - Ezh

4

简短版

delete 操作符总是有效的,并且提供更易于理解的错误信息。不应再使用私有声明来防止调用!

详细说明

如果将函数声明为私有,则仍然可以调用该函数。例如,在 staticfriend 函数中,可以调用构造函数。

通过显式删除函数,您表达了它永远不应该被使用。这尤其在尝试调用此类函数时会导致清晰易懂的错误消息。

在下面的类中,我们没有默认构造函数,因为没有有意义的方法来实现它。该类包含一个引用,需要一些对象来指向。

class Foo {
private:
    int& ref_;

public:
    Foo(int& ref) : ref_(ref) {}
};

int main() {
    Foo();
}

$ g++ main.cpp
main.cpp: In function ‘int main()’:
main.cpp:10:9: error: no matching function for call to ‘Foo::Foo()’
   10 |     Foo();
      |         ^
main.cpp:6:5: note: candidate: ‘Foo::Foo(int&)’
    6 |     Foo(int& ref) : ref_(ref) {}
      |     ^~~
main.cpp:6:5: note:   candidate expects 1 argument, 0 provided
main.cpp:1:7: note: candidate: ‘constexpr Foo::Foo(const Foo&)’
    1 | class Foo {
      |       ^~~
main.cpp:1:7: note:   candidate expects 1 argument, 0 provided
main.cpp:1:7: note: candidate: ‘constexpr Foo::Foo(Foo&&)’
main.cpp:1:7: note:   candidate expects 1 argument, 0 provided

删除构造函数可以使错误信息更简短易懂。

class Foo {
private:
    int& ref_;

public:
    Foo() = delete;
    Foo(int& ref) : ref_(ref) {}
};

int main() {
    Foo();
}

$ g++ main.cpp
main.cpp: In function ‘int main()’:
main.cpp:11:9: error: use of deleted function ‘Foo::Foo()’
   11 |     Foo();
      |         ^
main.cpp:6:5: note: declared here
    6 |     Foo() = delete;
      |     ^~~

如果我们通过私有声明来解决这个问题(没有定义,这是不可能的),一开始看起来消息会很相似。
class Foo {
private:
    int& ref_;

    Foo();

public:
    Foo(int& ref) : ref_(ref) {}
};

int main() {
    Foo();
}

$ g++ main.cpp
main.cpp: In functionint main()’:
main.cpp:12:9: error: ‘Foo::Foo()’ is private within this context
   12 |     Foo();
      |         ^
main.cpp:5:5: note: declared private here
    5 |     Foo();
      |     ^~~

只要你不从类的私有部分可访问的上下文中调用构造函数,这个方法就非常有效。就像上面所写的那样,这可以是一个静态函数或友好函数。原则上,它也可以是普通函数,尽管这种用例相当罕见。

class Foo {
private:
    int& ref_;

    Foo();

public:
    Foo(int& ref) : ref_(ref) {}

    static Foo create() {
        return Foo(); // compiles fine
    }

    void foo() {
        Foo(); // compiles fine
    }

    friend void bar();
};

void bar() {
    Foo(); // compiles fine
}

int main() {}

g++ -c -o main.o main.cpp

这个程序可以完全编译通过,编译器只是假设 Foo::Foo() 的定义会在其他地方出现。一旦链接器需要将其转换为可执行文件时,它就会报告缺少定义的错误。

$ g++ main.o
# or as one step with compilation and linking
$ g++ main.cpp
/usr/bin/ld: /tmp/ccnhLDsv.o: in function `bar()':
main.cpp:(.text+0x23): undefined reference to `Foo::Foo()'
collect2: error: ld returned 1 exit status

这种错误通常非常难以调试,因为它们可能卡在代码库的任何位置,而您没有线索知道错误出现在哪个文件,更不用说哪一行了。

另一方面,显式的delete会在三个位置提供三个精确的错误信息,指明错误所在的位置。

class Foo {
private:
    int& ref_;

public:
    Foo() = delete;
    Foo(int& ref) : ref_(ref) {}

    static Foo create() {
        return Foo(); // error
    }

    void foo() {
        Foo(); // error
    }

    friend void bar();
};

void bar() {
    Foo(); // error
}

int main() {}

$ g++ main.cpp
main.cpp: In static member functionstatic Foo Foo::create()’:
main.cpp:10:20: error: use of deleted functionFoo::Foo()’
   10 |         return Foo(); // error
      |                    ^
main.cpp:6:5: note: declared here
    6 |     Foo() = delete;
      |     ^~~
main.cpp: In member functionvoid Foo::foo()’:
main.cpp:14:13: error: use of deleted functionFoo::Foo()’
   14 |         Foo(); // error
      |             ^
main.cpp:6:5: note: declared here
    6 |     Foo() = delete;
      |     ^~~
main.cpp: In functionvoid bar()’:
main.cpp:21:9: error: use of deleted functionFoo::Foo()’
   21 |     Foo(); // error
      |         ^
main.cpp:6:5: note: declared here
    6 |     Foo() = delete;
      |     ^~~

附加信息

void foo(int need_integer) {}

int main() {
    foo(5.4); // might trigger a warning, but compiles
}

请注意,delete也可用于普通函数。例如,可以防止隐式转换。
void foo(int need_integer) {}
void foo(double) = delete;

int main() {
    foo(5);   // okay
    foo(5.4); // error
}

$ g++ main.cpp
main.cpp: In function ‘int main()’:
main.cpp:6:8: error: use of deleted function ‘void foo(double)’
    6 |     foo(5.4); // error
      |     ~~~^~~~~
main.cpp:2:6: note: declared here
    2 | void foo(double) = delete;
      |      ^~~

很棒的输入。在处理编译错误时真的很有帮助。 - undefined

2
我曾在LLVM源代码(例如在AlignOf.h中)中遇到过被声明为“deleted”的默认构造函数。相关的类模板通常位于名为“llvm::detail”的特殊命名空间中。我认为整个目的是他们认为这些类只是辅助类,从未打算实例化它们;只是在其他类模板的上下文中使用它们进行一些编译时的元编程技巧。例如,有一个名为AlignmentCalcImpl的类模板,它仅作为AlignOf类模板的参数使用,并用于sizeof(.)操作符。该表达式可以在编译时计算出来,而且没有必要实例化模板,所以为什么不声明默认构造函数删除以表达这种意图呢?但这只是我的假设。

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接