【问题标题】:Uses of destructor = delete;析构函数的使用 = 删除;
【发布时间】:2017-04-06 04:34:50
【问题描述】:

考虑以下类:

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

简而言之,出于问题的目的:我无法创建 S 的实例,例如 S s{};,因为我无法销毁它们。
正如 cmets 中提到的,我仍然可以通过 S *s = new S; 创建一个实例,但我也不能删除它。
因此,我可以看到删除的析构函数的唯一用途是这样的:

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

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

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

已删除的析构函数还有哪些其他用途(如果有)?

【问题讨论】:

  • 在这种情况下当然可以创建S的实例,只需使用new S
  • @skypjack 即使删除了析构函数,您也可以使用placement-new 创建(和删除)。也许这可能有一些用途,您希望全局对象管理不同的子对象并且您不希望这些子对象存在于容器之外......
  • 也就是说,定义一个只公开一堆静态函数的类,并禁止任何尝试创建该类的实例。又名命名空间?
  • @Borgleader 事实上,我可以用另一种方式做到这一点,这里有什么帮助?我不是要求替代方法,我已经知道它们,我只是想知道destructor = delete; 的用途是什么。
  • @Borgleader 具有静态方法的对象可以被模板化,命名空间不能。

标签: c++ c++11 destructor


【解决方案1】:

如果您有一个永远不应该成为deleted 或存储在堆栈中(自动存储)或存储为另一个对象的一部分的对象,=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;
}

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

我相信以上都不是未定义的行为;未能销毁 Bundle 是可以接受的,在其位置创建一个新的也是可以接受的。

并且界面不必公开Bundle 的工作方式。只是一个不透明的Handle

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

【讨论】:

  • ~Handle=delete(); 应该是~Handle()=delete; 吗?
  • @immibis 什么,=delete 不与() 通勤?
【解决方案2】:

一种情况可能是防止错误释放:

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

【讨论】:

  • 将这种方法称为“正确”有点奇怪。在 C++ 中,您通常会尽量避免使用 mallocfree
  • 我猜这里的意图是,使用一些自定义的分配和释放(工厂)方式,而不是newdelete。使用my_allocmy_dealloc 之类的名称可能会更清楚; mallocfree 这两个名字只是一个非常糟糕的例子。此外,my_alloc 应该返回一个正确的指针(不是void*)。
  • 我仍然真的不认为这是正确的 C++
  • malloc 可能会出现在混合环境中...我添加了一个更好的示例
  • 我投了赞成票,并警告说更好的 C++11 实现将使用 std::unique_ptr&lt;&gt; 来实现工厂析构函数,但这样做会掩盖所演示的机制。所以这是一个很好的例子,但不是正确的实现。
【解决方案3】:

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

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

有哪些用例?

我可以看到至少 3 种不同的用途:

  1. 永远不应实例化该类;在这种情况下,我还希望删除默认构造函数。
  2. 应该泄露这个类的一个实例;例如,一个日志记录单例实例
  3. 此类的实例只能通过特定机制创建和释放;使用 FFI 时尤其会发生这种情况

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

struct Handle { /**/ };

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

在 C++ 中,您可能希望将其包装在 unique_ptr 中以自动发布,但如果您不小心写了:unique_ptr&lt;Handle&gt; 怎么办?这是一场运行时灾难!

因此,您可以调整类定义:

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

然后编译器将阻塞unique_ptr&lt;Handle&gt;,迫使您正确使用unique_ptr&lt;Handle, xyz_dispose&gt;

【讨论】:

  • unique_ptr&lt;Handle&gt;真的是一场灾难吗?只要unique_ptr是用xyz_create中的有效删除器xyz_dispose构造的,一旦返回unique_ptr&lt;Handle, xyz_dispose&gt;,就会用xyz_dispose构造的删除器将构造移动到unique_ptr&lt;Handle&gt;,不是吗?跨度>
  • @wasthishelpful:不;与 shared_ptr 类型擦除类型的析构函数不同,unique_ptr 没有间接性。另外,请注意xyz_create 是一个 C 函数,它返回一个裸骨Handle*,而不是unique_ptr。最后,如果有人尝试从unique_ptr&lt;Handle, xyz_dispose&gt; 转换为unique_ptr&lt;Handle&gt;,我预计会出现编译器错误。
  • 如果您正在调整定义(大概在#ifdef 中),为什么不写~Handle() {xyz_dispose(this);} 而不是删除析构函数?现在,unique_ptr&lt;Handle&gt; 会自动执行明智的操作,而不是让您在每次使用该类型时都编写处理程序(可以是 typedefed,但仍然......)
  • @LThode:我会亲自围绕std::unique_ptr&lt;Handle, xyz_dispose&gt; 编写一个类,它会更容易使用(比重复调用.get() 来访问原始指针)并提供封装(客户端类将与 C 接口的更改隔离)。这是个人选择 :) 无论如何,我并不是说这些例子很棒。我是说他们是可能的。我只在个人情况(1)中使用过这个。
  • C 接口如何返回带有已删除析构函数的类?并且根据语言对该类使用不同的定义似乎不安全且不可移植。
【解决方案4】:

有两个可能的用例。首先(正如一些 cmets 所指出的)动态分配对象是可以接受的,不能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;
}

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

如果自动创建的析构函数会做一些不平凡的事情,您很可能会因未能执行其语义而损害程序的有效性。

让程序离开main() 并允许环境“清理”是一种有效的技术,但最好避免使用,除非限制非常必要。充其量这是掩盖真正内存泄漏的好方法!

我怀疑该功能是为了完整性而存在的,能够delete 其他自动生成的成员。

我希望看到此功能的真正实际应用。

有一个静态类的概念(没有构造函数),因此逻辑上不需要析构函数。但是这样的类更合适地实现为 namespace 在现代 C++ 中没有(好的)位置,除非模板化。

【讨论】:

  • 将对象实例化(并因此销毁)到预分配的缓冲区中在特殊环境中非常有用 - 例如,在嵌入式设备中或性能至关重要且内存分配/释放的开销过高(并导致碎片) )
  • @SomeWittyUsername 这很有用,甚至还有一个 Boost 库来实现分配池。当您需要修复开销或知道您将创建/删除许多实例时,它工作得很好。我所说的不是一个好主意,就是不调用析构函数。如果它是微不足道的,那么不会造成任何伤害。如果它不是微不足道的,你可能已经避免了一个问题。有效的方法是让执行结束并清理操作环境。我会修改我的 cmets。
  • 我希望看到此功能的实际应用。这或多或少是我提出这个问题的原因。抛开没有解释的反对票,我仍然想知道。 :-)
  • 具有静态方法的类可以被模板化,命名空间不能。更不用说特征类了;你是说 allocator_traits 是糟糕的现代 C++ 吗?我认为你的最后一段夸大了它的情况。
  • “特征示例是一个很好的观点。我仍然认为您想要一个包含函数模板集合的命名空间”我认为在大多数包含静态函数的特征的情况下,如果您尝试替换具有函数模板命名空间的特征,它在实践中会很差。因为,类模板可以部分特化,而函数模板不能。它们可以重载,但规则不同。此外,它通常会导致更糟糕的错误消息。我同意 Nir ​​Friedman 关于你最后一段的看法,这两件事听起来很相似,但实际上并不相同。
【解决方案5】:

使用new 创建一个对象的实例并且从不删除它是实现 C++ Singleton 的最安全方法,因为它避免了任何和所有破坏顺序问题。这个问题的一个典型例子是在另一个 Singleton 类的析构函数中访问的“Logging”Singleton。 Alexandrescu 曾经在他的经典“现代 C++ 设计”一书中用一整节来讨论如何处理 Singleton 实现中的破坏顺序问题。

删除的析构函数很好,这样即使 Singleton 类本身也不会意外删除实例。它还可以防止像delete &amp;SingletonClass::Instance() 这样的疯狂使用(如果Instance() 返回一个引用,它应该这样做;它没有理由返回一个指针)。

不过,归根结底,这并没有什么值得注意的。当然,你一开始就不应该使用单例。

【讨论】:

  • 如果您可以随时创建第二个单例,如何最安全地创建单例?
  • @rubenvb:通过将构造函数设为私有。否则它不是单例。但这是一个微不足道的细节,我什至没有提到它。
猜你喜欢
  • 2012-03-13
  • 2012-08-11
  • 1970-01-01
  • 1970-01-01
  • 2013-04-27
  • 2015-12-31
  • 2014-12-27
  • 2013-09-30
  • 2012-02-05
相关资源
最近更新 更多