析构函数中使用“=delete”的作用。

55
考虑以下类:

Consider the following class:

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

简单来说,我不能像这样创建实例:S s{};因为我无法销毁它们。
正如评论中提到的,我仍然可以通过这样做创建实例:S *s = new S;但我也无法删除它。
因此,我认为删除析构函数的唯一用途是这样的:

struct S {
    ~S() = delete;
    static void f() { }
};

int main() {
    S::f();
}

也就是说,定义一个只暴露一堆静态函数并禁止尝试创建该类实例的类。

如果有的话,删除析构函数还有哪些其他用途?


7
在这种情况下,你当然可以创建一个 S 的实例,只需使用 new S 即可。 - apple apple
4
即使析构函数被删除,使用定位new可以创建(和删除)对象。这在需要全局对象管理不同子对象,并且不希望这些子对象存在于容器之外的情况下可能会有一些用处。 - Holt
11
也就是说,定义一个类,只公开一堆静态函数,并禁止创建该类的任何实例。 这类似于命名空间吗? - Borgleader
3
@Borgleader 我能用另一种方式做到这一点的事实如何帮助这里?我不是在寻求替代方法,我已经知道它们,我只是好奇知道destructor = delete;的用途是什么。 - skypjack
10
@Borgleader反对使用静态方法的对象可以应用模板,但命名空间不行。 - Nir Friedman
显示剩余6条评论
5个回答

25

如果您有一个绝对不应该被delete或者存储在栈(自动存储)上的对象,或者作为另一个对象的一部分进行存储,那么使用=delete将会防止所有这些情况发生。

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

struct Data {
  std::array<char,1024> buffer;
};

struct Bundle: Handle {
  Data data;
};

using bundle_storage = std::aligned_storage_t<sizeof(Bundle), alignof(Bundle)>;

std::size_t bundle_count = 0;
std::array< bundle_storage, 1000 > global_bundles;

Handle* get_bundle() {
  return new ((void*)global_bundles[bundle_count++]) Bundle();
}
void return_bundle( Handle* h ) {
  Assert( h == (void*)global_bundles[bundle_count-1] );
  --bundle_count;
}
char get_char( Handle const* h, std::size_t i ) {
  return static_cast<Bundle*>(h).data[i];
}
void set_char( Handle const* h, std::size_t i, char c ) {
  static_cast<Bundle*>(h).data[i] = c;
}

这里我们有不透明的Handle,它们不能在堆栈上声明,也不能动态分配。我们有一个系统可以从已知数组中获取它们。

我认为以上没有未定义的行为; 不销毁Bundle是可以接受的,就像用新的Bundle替换旧的一样。

而且接口不必公开Bundle的工作原理。只需要一个不透明的Handle

如果代码的其他部分需要知道所有的Handle都在特定缓冲区中,或者它们的生命周期以特定方式进行跟踪,则此技术可能很有用。可能也可以使用私有构造函数和友元工厂函数来处理这个问题。


2
我认为,在不强制使用工厂(就像非公共构造函数的情况一样)的情况下,使自动(且我认为是静态的)存储期不可能是关键的。 - Peter - Reinstate Monica
2
~Handle=delete(); 应该改为 ~Handle()=delete;,是吧? - user253751
@immibis,=delete()不可交换吗? - Yakk - Adam Nevraumont
@ScienceDiscoverer 在99.99/100的情况下,您希望删除您创建的每个对象。在剩下的情况下,您确实希望对象的生命周期在程序结束时结束。这不是常见的用例,但OP要求提供任何用例。 - Yakk - Adam Nevraumont

16

一个可能的场景是防止错误的释放内存:

#include <stdlib.h>

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


int main() {

    S* obj= (S*) malloc(sizeof(S));

    // correct
    free(obj);

    // error
    delete obj;

    return 0;

}

这很基础,但适用于任何特殊的分配/释放过程(例如工厂)。

一个更符合'C++'风格的示例。

struct data {
    //...
};

struct data_protected {
    ~data_protected() = delete;
    data d;
};

struct data_factory {


    ~data_factory() {
        for (data* d : data_container) {
            // this is safe, because no one can call 'delete' on d
            delete d;
        }
    }

    data_protected* createData() {
        data* d = new data();
        data_container.push_back(d);
        return (data_protected*)d;
    }



    std::vector<data*> data_container;
};

10
将这种方法称为“正确”的说法有点奇怪。在C ++中,一般会尽量避免使用mallocfree - Lightness Races in Orbit
3
我猜这里的意图是,使用某种自定义的分配和释放方式(工厂),而不是使用 newdelete。如果使用类似于 my_allocmy_dealloc 这样的名称可能会更清晰;mallocfree 这些名称只是一个非常糟糕的例子。此外,my_alloc 应该返回一个正确的指针(而不是 void *)。 - anatolyg
1
我仍然认为这不是合适的C++。 - Bruno Ferreira
我已经点赞,但需要说明的是更好的C++11实现应该使用std::unique_ptr<>来实现工厂析构函数,但这样做会掩盖所展示的机制。因此,这是一个很好的例子,但不是正确的实现。 - Persixty
malloc和free在C++中有明确定义;这是实现new()的常用方式。在应用程序级别代码中,“通常会尝试避免使用malloc和free”,但在较低级别上完全有用。 - Peter K
显示剩余2条评论

12

为什么要将析构函数标记为delete

当然是为了防止析构函数被调用 ;)

这样做有什么用途?

我至少能看到三种不同的用途:

  1. 该类不应被实例化; 在这种情况下,我也期望有一个已删除的默认构造函数。
  2. 此类的实例应当被泄漏; 例如,一个日志单例实例
  3. 该类的实例只能通过特定机制创建和处理; 这可能会在使用FFI时发生

为了说明后一点,想象一个C接口:

struct Handle { /**/ };

Handle* xyz_create();
void xyz_dispose(Handle*);

在C++中,你会想要使用unique_ptr来自动释放内存,但如果你不小心写成:unique_ptr<Handle>,那就是运行时的灾难!

相反,你可以调整类定义:

struct Handle { /**/ ~Handle() = delete; };

接着编译器会卡在 unique_ptr<Handle> 上,迫使你正确使用 unique_ptr<Handle, xyz_dispose> 代替。


unique_ptr<Handle>真的是一场灾难吗?只要在xyz_create中使用有效的删除器xyz_dispose构造unique_ptr,一旦返回unique_ptr<Handle, xyz_dispose>,它将被移动构造为带有从xyz_dispose构造的删除器的unique_ptr<Handle>,不是吗? - rocambille
3
@wasthishelpful: 不是的;与类型擦除类型的析构函数的shared_ptr不同,unique_ptr没有间接性。此外,请注意xyz_create是一个C函数,它返回一个裸指针Handle*,而不是一个unique_ptr。最后,如果有人试图将unique_ptr<Handle, xyz_dispose>转换为unique_ptr<Handle>,我预计会出现编译器错误。 - Matthieu M.
2
如果您正在调整定义(可能在#ifdef中),为什么不编写~Handle() {xyz_dispose(this);}而不是删除析构函数?现在,unique_ptr<Handle>会自动执行明智的操作,而不是让您将处理器编写到类型的每个使用中(可以进行typedef,但仍然...)。 - LThode
1
@LThode:我个人会围绕std::unique_ptr<Handle, xyz_dispose>编写一个类,这样使用起来会更方便(比反复调用.get()获取原始指针更容易),并提供封装(类的客户端将与C接口的更改隔离开来)。这是个人选择 :) 无论如何,我并不是说这些示例很棒;我只是说它们是可能的。我个人只在情况(1)中使用过这种方法。 - Matthieu M.
一个 C 接口如何返回一个带有已删除析构函数的类?而且根据语言使用不同的定义来使用该类似乎是不安全和不可移植的。 - Jarod42
@Jarod42:如果函数是用C++编写并简单地公开了C接口,那么C接口可以轻松返回一个类。至于不安全/缺乏可移植性的问题,理论上(标准)可能会同意您的观点,但实际上没有问题(当然,只要您不使用virtual)。 - Matthieu M.

7

有两种可能的用例。首先(正如一些评论所指出的那样),可以动态分配对象,未能delete它们,并允许操作系统在程序结束时清理。

或者(更加奇怪的是),您可以分配一个缓冲区并在其中创建一个对象,然后删除缓冲区以恢复空间,但从不尝试调用析构函数。

#include <iostream>

struct S { 
    const char* mx;

    const char* getx(){return mx;}

    S(const char* px) : mx(px) {}
    ~S() = delete; 
};

int main() {
    char *buffer=new char[sizeof(S)];
    S *s=new(buffer) S("not deleting this...");//Constructs an object of type S in the buffer.
    //Code that uses s...
    std::cout<<s->getx()<<std::endl;

    delete[] buffer;//release memory without requiring destructor call...
    return 0;
}

除了在特殊情况下,这些都不是一个好主意。 如果自动创建的析构函数什么也不做(因为所有成员的析构函数都是平凡的),那么编译器将创建一个无效的析构函数。

如果自动创建的析构函数执行一些非平凡的操作,您很可能通过未能执行其语义来 compromise 您程序的有效性。

让程序离开 main() 并允许环境“清理”是一种有效的技术,但最好避免,除非约束条件使其绝对必要。 最多只是掩盖真正的内存泄漏问题!

我认为该功能出现是为了完整性,并具有删除其他自动生成的成员的能力。

我希望看到这种功能的真正实际用途。

有一个静态类(没有构造函数)的概念,因此逻辑上不需要析构函数。 但是,这样的类更适合实现为命名空间,并且除非使用模板,否则在现代C ++中不再有(好的)位置。


在特殊环境下,例如嵌入式设备或性能至关重要且内存分配/释放的开销过高(并导致碎片化)时,将对象实例化(因此销毁)到预分配的缓冲区中非常有用。 - SomeWittyUsername
1
我很想看到这种能力的真实实际应用。这也是我创建这个问题的原因。尽管没有解释就被踩,但我仍然渴望知道。 :-) - skypjack
@skypjack,它似乎是为了完整性而存在的,在那些你决定不调用它后果是良性的罕见情况下。这并不意味着=delete告诉编译器析构函数是微不足道的。它禁止导致其被调用的构造。 - Persixty
3
具有静态方法的类可以进行模板化,命名空间则不能。更不用说特性类了;您是在说 allocator_traits 不适用于现代 C++ 吗?我认为您最后一段过分强调了它的情况。 - Nir Friedman
2
“traits”示例是一个很好的观点。我仍然认为您想要一个包含函数模板集合的命名空间。在大多数包含静态函数的特性情况下,如果您尝试用函数模板的命名空间替换特性,实际上会非常糟糕。因为类模板可以部分特化,而函数模板不能。它们可以重载,但规则不同。此外,这通常会导致更糟糕的错误消息。我同意Nir friedman关于您最后一段的看法,这两件事可能听起来相似,但实际上并不相同。 - Chris Beck
显示剩余2条评论

5
使用new创建一个对象实例,且永远不删除它是实现C++单例模式最安全的方法,因为这样可以避免任何构造顺序和析构顺序的问题。一个典型的例子就是在另一个单例类的析构函数中访问“日志记录”单例。Alexandrescu曾在他的经典著作《现代C++设计》中专门讨论了应对单例实现中构造和析构顺序问题的各种方法。
删除析构函数很好,这样即使单例类本身也无法意外删除实例。它还可以防止像delete &SingletonClass::Instance()这样的疯狂用法(如果Instance()返回引用,则应该如此;没有理由返回指针)。
但总的来说,这些都不是真正值得注意的事情。当然,你根本不应该使用单例。

如果您随时可以创建第二个单例,那么如何才能以最安全的方式创建单例呢? - rubenvb
1
通过将构造函数设为私有,否则它就不是单例模式了。但这只是一个微不足道的细节,我甚至都没有提到它。 - Christian Hackl

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