【问题标题】:Creating and managing sublists of STL list创建和管理 STL 列表的子列表
【发布时间】:2014-12-19 05:05:53
【问题描述】:

我有一个问题的解决方案,该解决方案涉及从 STL 项目列表创建项目的子列表(即子范围)。例如,我有一个整数列表,需要创建子列表,其中子列表是一个类,它代表原始列表中的一系列项目。所有范围都是不相交的,我需要对子列表执行操作(插入/删除),这些更改需要反映在原始列表中。

我想出了下面的实现,它使用 C++11。但是,我在删除子列表时遇到了麻烦。问题是子列表的 _end 迭代器指向下一个连续子列表的 _beg 指针。所以我的问题是:

  1. 表示这种模式的常用方法是什么?即,管理列表的子列表,其中子列表中的修改反映在原始列表中。
  2. 我怎样才能更正下面的代码并且仍然使用 ranged-for 成语?

在这个例子中,子列表没有做很多事情,但这只是一个例子。在原始问题中,这两个类将用于几个不同的任务。

感谢任何帮助。

#include <iostream>
#include <list>
#include <algorithm>
using namespace std;

typedef list<int>           i_list;
typedef list<int>::iterator it_list;

class Sublist {
private:
    string  _name;
    i_list* _items;
    it_list _beg;
    it_list _end;

public:
    Sublist(string name, i_list* items, it_list beg, it_list end) 
        :   _name(name), 
            _items(items),
            _beg(beg), 
            _end(end)
    { 
    }

    it_list begin() { 
        return this->_beg; 
    }

    it_list end() { 
        return this->_end; 
    }

    void erase(it_list it) {
        if (it == _beg) {
            _beg = _items->erase(it);
        }
        else if (it == _end) {
            _end = _items->erase(it);
            _end--;
        }
        else { 
            _items->erase(it);
        }
    }

    void print() {
        cout << _name << ":" << endl;

        // this for access an invalid iterator
        for (auto x : *this) {
            cout << "\t" << x << endl;
        }
    }
};

class List {
private:
    i_list* items;

public:
    List(int size) { 
        items = new i_list();

        for (int i=1; i<=size; i++)
            items->push_back(i*10);
    }

    Sublist* sl(string name, int beg, int end) {
        return new Sublist( name, items, 
                                find(items->begin(), items->end(), beg),
                                find(items->begin(), items->end(), end));
    }
};


int main() {
    List f(20);

    Sublist* sl1 = f.sl("SL1", 10, 60);
    Sublist* sl2 = f.sl("SL2", 60, 110);
    Sublist* sl3 = f.sl("SL3", 110, 160);
    Sublist* sl4 = f.sl("SL4", 160, 210);

    sl1->print();
    sl2->print();
    sl3->print();
    sl4->print();

    sl2->erase( sl2->begin() );
    sl2->erase( sl2->begin() );
    sl2->erase( sl2->begin() );
    sl2->erase( sl2->begin() );
    sl2->erase( sl2->begin() );

    // When uncommented the program will get stuck looping
    //sl1->print();
    sl2->print();
    sl3->print();
    sl4->print();

    return 0;
}

【问题讨论】:

  • 效果很好。我刚刚将 List::sl 方法中创建的 Sublist* 替换为新的 list(begin...end) 并且它起作用了。但为什么它会起作用?我认为通过这种方式元素会被复制,并且子列表中的更改不会反映在原始列表中......
  • 您在哪里检查,您的更改是否反映在原始列表中?
  • 你能显示你更改的代码吗?
  • @CaptainOblivious:在最坏的情况下,这将用它的 2^N 元素幂集替换 N 元素列表,因此可能不方便
  • @JohnTortugo:你的课堂上没有必要使用ilist* items= new ilist()。只需使用ilist items

标签: c++ list stl iterator


【解决方案1】:

您的问题是您将end 视为范围的一部分。参见例如erase 将接受 end在 C++ 中,end 是第一个不属于范围的元素。因此,对于相邻的子列表,end 与下一个 begin 基本相等。

因此,ranged-for 成语不会访问*end

【讨论】:

  • 我看到接收end 指针的擦除是一个问题。但现在我要解决的是擦除删除begin 迭代器后会发生什么。因为当这种情况发生时,先前的子列表 end 迭代器变得无效。你能详细说明你的答案吗?
  • 公平点。在内部存储倒数第二个迭代器(封闭范围)并从.end() 返回std::next(penultimate) 可能是一个有效的实现。这有一个很大的潜在问题:空子范围会有begin==end。这对您来说是个并发症吗?
  • 我认为这可能有效,但是当子列表为空时,我应该指向哪里开始和结束?如果他们指向不同子列表的项目,这对我来说似乎很难看。我想在每个子列表中使用哨兵值将是解决此问题的合理方法..
  • 这种方法的问题是,当子列表为空时,_end 迭代器无效,我们不能再使用_endend()。我设法在列表为空时引入了一个标志,因此您的建议也有效。
【解决方案2】:

首先,您的程序没有编译并且有一些逻辑错误,所以我不确定我是在解决您的实际问题,还是只是一个错字。

但是,您声称

所有范围都不相交

在语义上不正确,因为您通过两个不同的子列表访问某些元素(即由 begin 和 end _begin_end 迭代器指向的元素)。对于 STL 容器,C::end() 不需要 supposed 来指向实际元素,并且擦除或取消引用迭代器是未定义的行为。

在你的情况下我会做的是

1) 引入一个单独的标志来指示子列表为空

 private:
    string  _name;
    i_list* _items;
    bool _isEmpty;
    it_list _beg;
    it_list _end;

2) 将 _end 指向最后一个“有效”元素并通过以下方式实现end()

Sublist(string name, i_list* items, it_list beg, it_list end) 
    :   _name(name), 
        _items(items),
        _isEmpty(beg==end),
        _beg(beg),             
        _end(beg==end ? beg : end-1)
{ 
}

it_list end() {
    if (this->_isEmpty) {
        return this->_begin;   
    } else {    
         return this->_end+1; 
    }
}

3) 检查erase(),用户是否试图擦除范围内的最后一个元素

void erase(it_list it) {      
    if (it == _beg) {
        if (_beg==_end) {
            _isEmpty=true;                
        }
        _beg = _items->erase(it);
    }
    else if (it == _end) { //This would be UB in the original version anyway
        _end = _items->erase(it);
        _end--;
    }
    else { 
        _items->erase(it);
    }
}

4) 相应地调整您的 insert 方法(如果有的话)

从已经为空的范围中擦除元素 - 我相信 - 通常是 UB 无论如何,所以我没有检查,但你当然可以在擦除开始时添加这个检查并抛出错误。

==============

编辑:

我的解决方案还有一个问题:
调用sl2-&gt;erase( sl2-&gt;begin() ) 时 sl1->end() 的副本仍然无效,但我相信,解决此问题的唯一方法是引入服装迭代器或以某种方式修改原始数据结构 - 例如将一些额外的保护节点拼接到原始列表中

作为对 MSalters 评论的回应,我想详细说明一下我认为的问题:
取决于一个人关注的是实现还是行为 - 并忽略了所有空子列表 - 您可以将其视为单个设计错误或行为中的一个确定和一个潜在错误。让我们从最后一个开始:

由于没有单独的文档,可以说Sublist::erase() 的代码暗示sl1-&gt;erase( sl1-&gt;end() ) 应该可以工作,但是由于它破坏了sl2,因此存在缺陷。但是,也有人可能会争辩说 - 期待 STL 容器语义 - 这个调用无论如何都是 UB,因此,该类的行为“正确”,因为破坏 sl2 只是 UB 的一种可能化身。
main() 中的示例代码实际上遇到的更大问题是调用 sl2-&gt;erase( sl2-&gt;begin() ) 破坏了 sl1,我相信没有人会说这是可以接受的行为。

现在,行为中的这两个错误都是单一设计问题的结果,即相邻子列表的 _begin_end 迭代器指向父列表中的相同节点 彼此。从sl1.end()sl2.begin() 返回的迭代器指向同一个内存位置这一事实本身并不一定是问题,但是当调用sl2-&gt;erase( sl2-&gt;begin() ) 时,这不仅会破坏_end 指向的元素,但也会切断通向该节点的链接。结果,基于范围的 for 循环永远不会到达sl1.end(),并且由于该链表似乎是作为循环列表实现的,因此会导致无限循环。

【讨论】:

  • "开始和结束迭代器指向的[元素]" - 结束迭代器不指向范围内的元素。 C++ 和 C 一样,使用半开区间:[begin, end)。这正是为什么相邻范围可以共享它们的结束/开始迭代器的原因。
  • @MSalters:是的,但首先,正如您自己指出的那样,代码的语义意味着 end() 被视为子范围的一部分。其次,我指的是内部迭代器 _end 和 _begin。对于链表,如果它们指向同一个元素,即使 end() 从未取消引用或直接删除,这是一个问题,因为修改一个子列表的开始或结束会使存储在相邻的迭代器中的相应迭代器无效(隐藏共享状态)。当我有时间时,我会修改我的答案以详细说明这一点。
  • @MikeMB:当我使用带有 -std=c++11 的 g++ 时,这里的代码编译得很好。你得到了什么错误?此代码中列表的点不是“开始脱节”正是我想要解决的问题。如果代码中还有其他“错误”,则它们不是我寻求帮助的原因,并且可能在“擦除问题”解决之前我不想处理它们。您能否发布您建议的仍然使用ranged-for idiom 的实现,正如我在问题中所问的那样?谢谢!
  • 我看到你已经编辑了你的答案,包括一些解释......和来自 cmets 的文本。那没问题。但一个有趣的部分是,我的问题正是为您的“解决方案”不起作用(您的话)寻求解决方案:“调用 sl2->erase( sl2->开始()),...”。事实上,我在每个子列表之后引入了一个哨兵值,以便每个子列表的 _end 都指向这个哨兵。
  • @JohnTortugo:简而言之,在我正确理解您的代码和问题之前,我想我过早地回答了。我对此表示最诚挚的歉意。我认为我的答案可以解决您的问题的原因是您在 main 中的示例代码应该可以完美运行,并且您可以使用基于范围的 for 循环,除非您在其中例如在遍历 sl1 时删除 sl2 的第一个元素。同样在任何时候,您都可以正确检索子列表的当前结束位置,这在以前是不可能的,您只是无法在所有情况下缓存它。
猜你喜欢
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 2013-06-06
  • 1970-01-01
  • 1970-01-01
  • 2019-08-12
  • 2014-03-11
相关资源
最近更新 更多