【问题标题】:calling destructor in custom container在自定义容器中调用析构函数
【发布时间】:2016-11-05 08:35:12
【问题描述】:

我正在设计一个这样的自定义 C++ 模板容器:

template <class C>
class Container {
public:
    ...
    void clear() {
        // should I call destructor on elements here?
        for (i...)
            array[i].~C(); // ?
    }

    ~Container() {
        delete [] array_;
    }
private:
    C* array_;
};

我应该在 clear() 中手动调用元素的析构函数吗?如果我不这样做,它们将在容器被销毁时被调用(因为我在析构函数中删除 [] array_),但这不是预期的行为(我们希望它们在 clear() 中被销毁,就像 std ::vector 确实如此)。 但是,如果我确实调用了析构函数,那么该内存仍然存在,并且当我将在旧元素之上添加新元素(现在已被销毁)时,将对那些被销毁的元素调用赋值运算符,这可能会导致在未定义的行为中。 假设我有一堂课:

class Foo {
public:
    Foo() { foo_ = new int; }
    ~Foo() { delete foo_; } // note I don't explicitly set foo_ to nullptr here
    Foo(Foo &&f) {
        f.foo_ = std::exchange(foo_, f.foo_); // standard practice in move constructors
    }
};

好的,到目前为止这看起来不错,但是现在如果我做一个

Container<Foo> c;
c.add(Foo());
c.clear();
c.add(Foo());

在调用 clear() 时,会调用初始 Foo 的析构函数,使其 foo_ 指针悬空。接下来,当添加第二个 Foo 时,临时 R 值与被破坏对象的旧内容交换,当 temp 将被破坏时,其析构函数将尝试再次删除相同的悬空指针,这将崩溃。

那么,如何正确地清除()一个容器而不留空间进行双重删除呢? 我还读到它建议不要在析构函数中设置指向 nullptr 的指针,以免隐藏潜在的悬空指针问题。

我对如何处理这个问题有点困惑。

编辑:-------

对于模板容器似乎没有不折不扣的方法。 到目前为止,我看到了这些选项,正如其他人指出的那样:

  1. 在clear()中彻底删除底层数组;这将是 安全但不高效。但是我不能使用这个选项,因为我 实现无锁多线程容器。
  2. 我不会在 clear() 中调用元素的析构函数,而是调用默认构造函数:array[i] = C();这似乎 就像迄今为止最好的解决方案一样 - 除了它仍然意味着额外的 默认构造。
  3. 在 add() 中使用新位置 - 这似乎适用于复制构造。 但是,移动构造到未初始化的对象似乎仍然存在问题,因为大多数移动构造函数都是通过指针之间的交换来实现的——这会使源对象(被移动)无效。

【问题讨论】:

  • 只是出于好奇,您如何定义 Container::add?您是否有预分配的未初始化单元格,还是每次都重新分配整个数组?
  • @Necto,我有一个预分配的未初始化单元格数组,并使用复制构造或移动构造(取决于如何调用 add )。如果我没有遗漏什么,就像 std::vector 一样
  • 你如何分配那个数组?
  • array = new C[容量];所以数组包含 C 类型的对象,这就是诀窍。当它们被破坏时,它们仍然存在,并且它们的 = 运算符或复制/移动构造函数仍然被调用
  • @BogdanIonitza new not 分配未初始化的元素。我猜你的意思是 default-constructed。两者是截然相反的概念。我明白你的意思,但我们都知道在使用 C++ 中的术语时需要多么小心!

标签: c++ c++11 containers


【解决方案1】:

delete [] array_ 将为array_ 的每个元素调用析构函数(假设C 是具有析构函数的类型)。

对任何给定对象调用析构函数两次会产生未定义的行为。

这意味着您的clear() 成员函数不应直接调用数组元素的析构函数。

如果你坚持有一个单独的clear() 函数,它不会干扰析构函数的工作,只需将其实现为

void clear()
{
    delete [] array_;
    array = nullptr;       //  NULL or 0 before C++11
}

这不会干扰析构函数,因为操作符 delete 在作用于 NULL 指针时无效。

【讨论】:

  • 好的,这会起作用,并且不会受到我最初建议的任何缺点的影响。但是,由于必须在每次 clear() 之后重新分配数组,因此会导致性能损失。从我设法从 STL 头文件中解密的内容来看,std::vector 不会删除其在 clear() 上的底层数组,只是调用了易受我在帖子中描述的相同问题影响的析构函数
  • @BogdanIonitza 我猜,STL 也没有delete [] array,它在不调用析构函数的情况下释放内存,因为它很可能未初始化。
  • STL 实现旨在使用 placement new 成语。你也可以做到。
【解决方案2】:

正如您在 cmets 中指定的那样,在未分配的单元格中,您拥有由默认构造函数 (T()) 构造的对象。这可能对性能不利,但肯定会保留对象抽象。 不要删除条目,只需将它们替换为默认构造函数构造的条目即可:

template <class C>
class Container {
public:
    ...
    void clear() {
        for (i...)
            array_[i] = C();
    }

    ~Container() {
        delete [] array_;
    }
private:
    C* array_;
};

替代实现

另一方面,我会提出一种性能更高的方法,尽管它破坏了漂亮的 C++ 抽象。您可以在不调用不必要的默认构造函数的情况下分配内存:

template <class C>
class Container {
public:
    Container(int n) {
        array_ = (C*)malloc(sizeof(C)*n);
        size_ = 0;
    }

    ...

    void clear() {
        for (... i < size_ ...)
            array_[i].~C();
        size_ = 0;
    }

    ~Container() {
        clear();
        free(array_);
    }
private:
    C* array_;
    int size_;
};

在这里,您确实对所有已初始化的元素调用了析构函数,但您不会第二次调用它,因为您会跟踪哪些已初始化,哪些未初始化。

【讨论】:

  • “未初始化”和“由默认构造函数构造”是截然相反的概念。我明白你的意思,但我们都知道在使用 C++ 中的术语时需要非常小心!
  • @underscore_d,谢谢,我用“未分配”替换了这个词。
  • 很好的答案@Necto!用默认构造的对象替换它们肯定会起作用,尽管它不会对性能非常友好。第二种方式,使用手动分配的内存与第一种方式在 add()-ing 对象(通过移动或复制)时没有任何不同,因为我们仍然存在在复制到目标时自动调用析构函数的问题(或销毁移出的源对象时)
  • @BogdanIonitza,第二种方法与adding 有很大不同:使用placement new 直接在未初始化的单元格中调用复制构造函数。 new (array_+size_) C(ref)
  • @Necto,但是在未初始化的对象上调用复制构造函数(就像在手动分配的内存上放置新的情况一样)可能会崩溃(因为在目标之前没有自动调用析构函数赋值):假设你在构造函数中有一个手动分配的ptr,在复制构造函数中你需要在获取新值之前释放它,否则它会泄漏)
猜你喜欢
  • 1970-01-01
  • 2012-04-29
  • 1970-01-01
  • 2019-02-22
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 2013-04-26
  • 1970-01-01
相关资源
最近更新 更多