草庐IT

c++ - 用空实现覆盖删除运算符

coder 2023-05-31 原文

在对象上使用 delete 运算符通常会导致两件事:调用对象的析构函数(及其虚拟基析构函数,如果存在)并随后释放内存。

如果重写一个类的 delete 操作符,给它一个空的实现 {},析构函数仍会被调用,但内存不会被释放。

假设析构函数也是空的,那么 delete 是否会有任何影响,或者继续使用“已删除”对象是否安全(即是否存在未定义的行为)?

struct Foo {
    static void operator delete(void* ptr) {}
    Foo() {}
    ~Foo() {}
    void doSomething() { ... }
}

int main() {
    Foo* foo = new Foo();
    delete foo;
    foo->doSomething(); // safe?
}

并不是说这很有意义,但我正在研究一种“延迟删除”(gc)机制,当调用 delete 时,对象不会立即被删除,但很快之后。

更新

引用一些提到内存泄漏的答案:让我们假设重载的 delete 运算符不为空,但确实将其 ptr 参数存储在一个(假设 static,为简单起见)set:

struct Foo {
    static std::unordered_set<void*> deletedFoos;
    static void operator delete(void* ptr) {
        deletedFoos.insert(ptr);
    }
    Foo() {}
    ~Foo() {}
}

并且这个 set 会定期清理:

for (void* ptr : Foo::deletedFoos) {
    ::operator delete(ptr);
}
Foo::deletedFoos.clear();

最佳答案

来自 n4296:

A destructor is invoked implicitly

(11.1) — for a constructed object with static storage duration (3.7.1) at program termination (3.6.3),

(11.2) — for a constructed object with thread storage duration (3.7.2) at thread exit,

(11.3) — for a constructed object with automatic storage duration (3.7.3) when the block in which an object is created exits (6.7),

(11.4) — for a constructed temporary object when its lifetime ends (12.2).

In each case, the context of the invocation is the context of the construction of the object. A destructor is also invoked implicitly through use of a delete-expression (5.3.5) for a constructed object allocated by a new-expression (5.3.4); the context of the invocation is the delete-expression. [ Note: An array of class type contains several subobjects for each of which the destructor is invoked. —end note ] A destructor can also be invoked explicitly.

因此,调用 delete 运算符的 delete 表达式的使用,也隐式调用了析构函数。对象的生命结束了,如果您为该对象调用方法,则会发生未定义的行为。

#include <iostream>

struct Foo {
    static void operator delete(void* ptr) {}
    Foo() {}
    ~Foo() { std::cout << "Destructor called\n"; }
    void doSomething() { std::cout << __PRETTY_FUNCTION__ << " called\n"; }
};

int main() {
    Foo* foo = new Foo();
    delete foo;
    foo->doSomething(); 
   // safe? No, an UB. Object's life is ended by delete expression.
}

输出:

Destructor called
void Foo::doSomething() called

使用:gcc HEAD 8.0.0 20170809 with -O2

问题首先假设重新定义删除运算符和对象的行为将省略对象的破坏。重新定义对象本身的析构函数不会重新定义其字段的析构函数。 事实上,从语义的角度来看,它不再存在了。它不会释放内存,如果对象存储在内存池中,这可能是一件事情。但可以这么说,它会删除对象的抽象“灵魂”。之后调用方法或访问对象的字段是UB。 在特定情况下,根据操作系统,该内存可能永远保持分配状态。这是一种不安全的行为。假设编译器会生成合理的代码也是不安全的。它可能会完全省略操作。

让我向对象添加一些数据:

struct Foo {
    int a;
    static void operator delete(void* ptr) {}
    Foo(): a(5) {}
    ~Foo() { std::cout << "Destructor called\n"; }
    void doSomething() { std::cout << __PRETTY_FUNCTION__ << "a = " << a << " called\n"; }
};

int main() {
    Foo* foo = new Foo();
    delete foo;
    foo->doSomething(); // safe?
}

输出:

Destructor called
void Foo::doSomething() a= 566406056 called

嗯?我们没有初始化内存?让我们在销毁之前添加相同的调用。

int main() {
    Foo* foo = new Foo();
    foo->doSomething(); // safe!
    delete foo;
    foo->doSomething(); // safe?
}

在这里输出:

void Foo::doSomething() a= 5 called
Destructor called
void Foo::doSomething() a= 5 called

什么?当然,编译器只是在第一种情况下省略了 a 的初始化。可能是因为类没有做任何其他事情吗?在这种情况下是可能的。但是这个:

struct Foo {
    int a, b;
    static void operator delete(void* ptr) {}
    Foo(): a(5), b(10) {}
    ~Foo() { std::cout << "Destructor called\n"; }
    void doSomething() { std::cout << __PRETTY_FUNCTION__ << " a= " << a << " called\n"; }
};

int main() {
    Foo* foo = new Foo();
    std::cout << __PRETTY_FUNCTION__ << " b= " << foo->b << "\n"; 
    delete foo;
    foo->doSomething(); // safe?
}

会产生类似的未定义值:

int main() b= 10
Destructor called
void Foo::doSomething() a= 2017741736 called

编译器认为 a 字段在 foo 死亡时未使用,因此“死亡”而不影响进一步的代码。 foo 与所有“手”一起倒下,并且它们中的任何一个都不再正式存在。更不用说在 Windows 上,使用 MS 编译器,当 Foo::doSomething() 尝试恢复死成员时,这些程序可能会崩溃。 Placement new 可以让我们扮演 Dr.Frankenstein 的角色:

    #include <iostream>
#include <new>
struct Foo {
    int a;
    static void operator delete(void* ptr) {}
    Foo()              {std::cout << __PRETTY_FUNCTION__ << " a= " << a << " called\n"; }
    Foo(int _a): a(_a) {std::cout << __PRETTY_FUNCTION__ << " a= " << a << " called\n"; }
    ~Foo() { std::cout << "Destructor called\n"; }
    void doSomething() { std::cout << __PRETTY_FUNCTION__ << " a= " << a << " called\n"; }
};

int main() {
    Foo* foo = new Foo(5);
    foo->~Foo(); 

    Foo *revenant = new(foo) Foo();
    revenant->doSomething(); 
}

输出:

Foo::Foo(int) a= 5 called
Destructor called
Foo::Foo() a= 1873730472 called
void Foo::doSomething() a= 1873730472 called

不管我们是否调用析构函数,编译器都可以决定 revenant 与原始对象不同,因此我们不能重用旧数据,只能重用内存。

奇怪的是,在仍然执行 UB 的同时,如果我们从 Foo 中删除删除操作符,该操作似乎与 GCC 一样工作。在这种情况下我们不调用 delete,但是删除和添加它会改变编译器的行为,我相信这是实现的产物。

关于c++ - 用空实现覆盖删除运算符,我们在Stack Overflow上找到一个类似的问题: https://stackoverflow.com/questions/45628694/

有关c++ - 用空实现覆盖删除运算符的更多相关文章

  1. ruby-on-rails - 如何从 format.xml 中删除 <hash></hash> - 2

    我有一个对象has_many应呈现为xml的子对象。这不是问题。我的问题是我创建了一个Hash包含此数据,就像解析器需要它一样。但是rails自动将整个文件包含在.........我需要摆脱type="array"和我该如何处理?我没有在文档中找到任何内容。 最佳答案 我遇到了同样的问题;这是我的XML:我在用这个:entries.to_xml将散列数据转换为XML,但这会将条目的数据包装到中所以我修改了:entries.to_xml(root:"Contacts")但这仍然将转换后的XML包装在“联系人”中,将我的XML代码修改为

  2. ruby - 我可以使用 Ruby 从 CSV 中删除列吗? - 2

    查看Ruby的CSV库的文档,我非常确定这是可能且简单的。我只需要使用Ruby删除CSV文件的前三列,但我没有成功运行它。 最佳答案 csv_table=CSV.read(file_path_in,:headers=>true)csv_table.delete("header_name")csv_table.to_csv#=>ThenewCSVinstringformat检查CSV::Table文档:http://ruby-doc.org/stdlib-1.9.2/libdoc/csv/rdoc/CSV/Table.html

  3. ruby-on-rails - 在混合/模块中覆盖模型的属性访问器 - 2

    我有一个包含模块的模型。我想在模块中覆盖模型的访问器方法。例如:classBlah这显然行不通。有什么想法可以实现吗? 最佳答案 您的代码看起来是正确的。我们正在毫无困难地使用这个确切的模式。如果我没记错的话,Rails使用#method_missing作为属性setter,因此您的模块将优先,阻止ActiveRecord的setter。如果您正在使用ActiveSupport::Concern(参见thisblogpost),那么您的实例方法需要进入一个特殊的模块:classBlah

  4. ruby-on-rails - 如何优雅地重启 thin + nginx? - 2

    我的瘦服务器配置了nginx,我的ROR应用程序正在它们上运行。在我发布代码更新时运行thinrestart会给我的应用程序带来一些停机时间。我试图弄清楚如何优雅地重启正在运行的Thin实例,但找不到好的解决方案。有没有人能做到这一点? 最佳答案 #Restartjustthethinserverdescribedbythatconfigsudothin-C/etc/thin/mysite.ymlrestartNginx将继续运行并代理请求。如果您将Nginx设置为使用多个上游服务器,例如server{listen80;server

  5. ruby - 我可以使用 aws-sdk-ruby 在 AWS S3 上使用事务性文件删除/上传吗? - 2

    我发现ActiveRecord::Base.transaction在复杂方法中非常有效。我想知道是否可以在如下事务中从AWSS3上传/删除文件:S3Object.transactiondo#writeintofiles#raiseanexceptionend引发异常后,每个操作都应在S3上回滚。S3Object这可能吗?? 最佳答案 虽然S3API具有批量删除功能,但它不支持事务,因为每个删除操作都可以独立于其他操作成功/失败。该API不提供任何批量上传功能(通过PUT或POST),因此每个上传操作都是通过一个独立的API调用完成的

  6. ruby - 触发器 ruby​​ 中 3 点范围运算符和 2 点范围运算符的区别 - 2

    请帮助我理解范围运算符...和..之间的区别,作为Ruby中使用的“触发器”。这是PragmaticProgrammersguidetoRuby中的一个示例:a=(11..20).collect{|i|(i%4==0)..(i%3==0)?i:nil}返回:[nil,12,nil,nil,nil,16,17,18,nil,20]还有:a=(11..20).collect{|i|(i%4==0)...(i%3==0)?i:nil}返回:[nil,12,13,14,15,16,17,18,nil,20] 最佳答案 触发器(又名f/f)是

  7. ruby - 无法覆盖 irb 中的 to_s - 2

    我在pry中定义了一个函数:to_s,但我无法调用它。这个方法去哪里了,怎么调用?pry(main)>defto_spry(main)*'hello'pry(main)*endpry(main)>to_s=>"main"我的ruby版本是2.1.2看了一些答案和搜索后,我认为我得到了正确的答案:这个方法用在什么地方?在irb或pry中定义方法时,会转到Object.instance_methods[1]pry(main)>defto_s[1]pry(main)*'hello'[1]pry(main)*end=>:to_s[2]pry(main)>defhello[2]pry(main)

  8. ruby - 如何根据特征实现 FactoryGirl 的条件行为 - 2

    我有一个用户工厂。我希望默认情况下确认用户。但是鉴于unconfirmed特征,我不希望它们被确认。虽然我有一个基于实现细节而不是抽象的工作实现,但我想知道如何正确地做到这一点。factory:userdoafter(:create)do|user,evaluator|#unwantedimplementationdetailshereunlessFactoryGirl.factories[:user].defined_traits.map(&:name).include?(:unconfirmed)user.confirm!endendtrait:unconfirmeddoenden

  9. ruby - 覆盖相似的方法,更短的语法 - 2

    在Ruby类中,我重写了三个方法,并且在每个方法中,我基本上做同样的事情:classExampleClassdefconfirmation_required?is_allowed&&superenddefpostpone_email_change?is_allowed&&superenddefreconfirmation_required?is_allowed&&superendend有更简洁的语法吗?如何缩短代码? 最佳答案 如何使用别名?classExampleClassdefconfirmation_required?is_a

  10. ruby - 是否可以覆盖 gemfile 进行本地开发? - 2

    我们的git存储库中目前有一个Gemfile。但是,有一个gem我只在我的环境中本地使用(我的团队不使用它)。为了使用它,我必须将它添加到我们的Gemfile中,但每次我checkout到我们的master/dev主分支时,由于与跟踪的gemfile冲突,我必须删除它。我想要的是类似Gemfile.local的东西,它将继承从Gemfile导入的gems,但也允许在那里导入新的gems以供使用只有我的机器。此文件将在.gitignore中被忽略。这可能吗? 最佳答案 设置BUNDLE_GEMFILE环境变量:BUNDLE_GEMFI

随机推荐