在 std::vector 中使用多态的 std::unique_ptr 时,如何使用 std::remove_if ?

9
我有一个由三个类组成的层次结构,其中 Derived 继承自 SelectableDrawable。然后,我有一个 std::vector,其中包含 Derived 对象,类型为 std::unique_ptr<Drawable>
我确信这个向量只会被同时从这两个基类继承的对象填充。
问题出现在我尝试使用指向 Selected 的指针从向量中删除某个元素时。
#include <vector>
#include <memory>
#include <algorithm>

struct Selectable {
    virtual ~Selectable() = 0;
};
Selectable::~Selectable() = default;

struct Drawable {
    virtual ~Drawable() = 0;
};
Drawable::~Drawable() = default;

struct Derived : Selectable, Drawable {};

int main()
{
    std::vector<std::unique_ptr<Drawable>> vec;
    for (int i = 0; i < 5; ++i) {
        vec.push_back(std::make_unique<Derived>());
    }
    Selectable* selected = dynamic_cast<Selectable*>(vec[2].get());

    vec.erase(std::remove_if(vec.begin(), vec.end(), 
        [selected](auto&& ptr) { 
            return ptr.get() == dynamic_cast<Drawable*>(selected); 
    }), vec.end());
}

显然,如果我将selected指向Drawable的指针,一切都很好,但这不是我的意图。
我遇到了一个运行时错误,导致程序崩溃。为什么会发生这种情况,我该如何解决?

1
哪个“runtime_error”? - The Techel
std::make_unique 是 C++14 中的函数。你应该在问题中打上 c++14 的标签。 - user184968
@SergeiKurenkov 我的错,没想到。谢谢。 - DeiDei
3个回答

10
关键问题在于std::remove_if的“删除”元素方式:
通过移动赋值来移位范围内的元素(即保留不需要删除的元素并将其移到范围的开始),剩下的元素的相对顺序保留,容器的物理大小不变。指向新逻辑结尾和范围物理结尾之间元素的迭代器仍然是可解引用的,但这些元素本身具有未指定的值(根据MoveAssignable后置条件)。
基本上,您保留了通过auto ptr = vec[2].get()获取的原始指针,但没有人保证ptr保持有效。您只保证vec [2] 有效。(在筛选之前在vec [2] 中使用的唯一指针现在位于新逻辑结尾和物理结尾之间,具有未指定的值)。
在您的示例中,当std::remove_if到达第三个元素时,谓词返回true,并调用remove_if vec[2].get()的析构函数。由于您保存了它的原始指针,因此您正在使用指向已经被销毁的对象的指针。

到达第二个元素 - [2] 是第三个元素。 - user184968
我明白了,那很有道理,但是我该如何删除这个元素呢? - DeiDei
3
使用 std::findstd::vector::erase 函数。 - David Haim

5
你的程序崩溃的原因是因为你对无效指针调用了dynamic_cast。很容易通过在析构函数中添加输出并打印所选内容来证明:
struct Selectable {
    virtual ~Selectable();
};
Selectable::~Selectable() {
  std::cout << "Selectable::~Selectable:" << this << std::endl;
};

struct Drawable {
    virtual ~Drawable();
};
Drawable::~Drawable() {
  std::cout << "Drawable::~Drawable:" << this << std::endl;
}

vec.erase(std::remove_if(vec.begin(), vec.end(), 
    [selected](auto&& ptr) { 
        std::cout << "selected:" << selected << std::endl;
        return ptr.get() == dynamic_cast<Drawable*>(selected); 
}), vec.end());

这是可能的输出:

$ ./a.exe
selected:0x3e3ff8
selected:0x3e3ff8
selected:0x3e3ff8
selected:0x3e3ff8
Drawable::~Drawable:0x3e3ffc
Selectable::~Selectable:0x3e3ff8
selected:0x3e3ff8
Segmentation fault

在一个无效的指针上调用 dynamic_cast 是未定义的行为

显然,如果我将 selected 改成指向 Drawable 的指针,一切都没问题,但这不是我的意图。

在这种情况下,您也有一个无效的指针,但是您的编译器简单地不生成 dynamic_cast,因为它不是必需的。结果,在这种情况下,您的程序避免崩溃。


0

在 Valgrind 下运行时,我看到的第一个错误是

Invalid read of size 8
   at 0x4CCE92D: __dynamic_cast (in /usr/lib/x86_64-linux-gnu/libstdc++.so.6.0.22)
   by 0x109139: _ZZ4mainENKUlOT_E_clIRSt10unique_ptrI8DrawableSt14default_deleteIS4_EEEEDaS0_ (43706186.cpp:27)
   by 0x10917B: _ZN9__gnu_cxx5__ops10_Iter_predIZ4mainEUlOT_E_EclINS_17__normal_iteratorIPSt10unique_ptrI8DrawableSt14default_deleteIS9_EESt6vectorISC_SaISC_EEEEEEbS2_ (predefined_ops.h:241)
   by 0x10902D: _ZSt11__remove_ifIN9__gnu_cxx17__normal_iteratorIPSt10unique_ptrI8DrawableSt14default_deleteIS3_EESt6vectorIS6_SaIS6_EEEENS0_5__ops10_Iter_predIZ4mainEUlOT_E_EEESE_SE_SE_T0_ (stl_algo.h:866)
   by 0x108F78: _ZSt9remove_ifIN9__gnu_cxx17__normal_iteratorIPSt10unique_ptrI8DrawableSt14default_deleteIS3_EESt6vectorIS6_SaIS6_EEEEZ4mainEUlOT_E_ESC_SC_SC_T0_ (stl_algo.h:937)
   by 0x108EBC: main (43706186.cpp:25)
 Address 0x5892dc0 is 0 bytes inside a block of size 16 free'd
   at 0x4A0A2DB: operator delete(void*) (in /usr/lib/valgrind/vgpreload_memcheck-amd64-linux.so)
   by 0x109BFC: Derived::~Derived() (43706186.cpp:15)
   by 0x109D21: std::default_delete<Drawable>::operator()(Drawable*) const (unique_ptr.h:76)
   by 0x10A7C4: std::unique_ptr<Drawable, std::default_delete<Drawable> >::reset(Drawable*) (unique_ptr.h:347)
   by 0x10A39D: std::unique_ptr<Drawable, std::default_delete<Drawable> >::operator=(std::unique_ptr<Drawable, std::default_delete<Drawable> >&&) (unique_ptr.h:254)
   by 0x109062: _ZSt11__remove_ifIN9__gnu_cxx17__normal_iteratorIPSt10unique_ptrI8DrawableSt14default_deleteIS3_EESt6vectorIS6_SaIS6_EEEENS0_5__ops10_Iter_predIZ4mainEUlOT_E_EEESE_SE_SE_T0_ (stl_algo.h:868)
   by 0x108F78: _ZSt9remove_ifIN9__gnu_cxx17__normal_iteratorIPSt10unique_ptrI8DrawableSt14default_deleteIS3_EESt6vectorIS6_SaIS6_EEEEZ4mainEUlOT_E_ESC_SC_SC_T0_ (stl_algo.h:937)
   by 0x108EBC: main (43706186.cpp:25)
 Block was alloc'd at
   at 0x4A0921F: operator new(unsigned long) (in /usr/lib/valgrind/vgpreload_memcheck-amd64-linux.so)
   by 0x10942E: std::_MakeUniq<Derived>::__single_object std::make_unique<Derived>() (unique_ptr.h:791)
   by 0x108DE2: main (43706186.cpp:21)

从这里我们可以看到,数组中添加的一个元素被删除了,但是我们仍然通过捕获的指针(selected)在 lambda 中尝试了 dynamic_cast
如果我们将转换移到 erase-remove 调用之外,则只有在删除元素之前执行一次 dynamic_cast
    auto const s2 = dynamic_cast<Drawable*>(selected);

    vec.erase(std::remove_if(vec.begin(), vec.end(), 
        [s2](auto&& ptr) { 
            return ptr.get() == s2; 
    }), vec.end());

这个版本在Valgrind中运行完成,没有任何警告。

顺便提一下,你可以编写lambda来接受const auto&,因为你不打算修改元素。


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