提问人:Bipolo 提问时间:9/29/2022 最后编辑:starballBipolo 更新时间:10/3/2022 访问量:215
共享“std::list”而不添加(冗余)引用
Sharing a `std::list` without adding a (redundant) reference to it
问:
我有一个 conainter,比如说一个 ,我想在对象之间共享它。已知其中一个物体比其他物体活得更长,因此他会握住容器。为了能够访问该列表,其他对象可能具有指向该列表的指针。
由于持有者对象可能会被移动,我需要用以下代码将列表包装起来:std::list<int>
unique_ptr
class LongLiveHolder { std::unique_ptr<std::list<int>> list; };
class ShortLiveObject { std::list<int>& list; };
但是,我真的不需要包装器。由于列表可能只包含指向第一个节点的 [] 指针(以及指向最后一个节点的指针),因此从理论上讲,我可以在其他对象上使用这些指针:unique_ptr
unique_ptr
class LongLiveHolder { std::unique_ptr<NonExistentListNode<int>> back; };
class ShortLiveObject { NonExistentListNode<int>& back; };
,这将在访问列表时为我节省多余的取消引用,除了我将不再拥有用于生存期较短的对象的完整接口 - 只有节点指针。std::list
我能否以某种方式摆脱这个额外的间接层,同时仍然在生存期较短的对象中拥有接口?std::list
答:
如果列表所有者将被移动,那么您需要一些内存地址以某种方式共享。
您已经指出了 .如果非所有者不需要在内部保存它,这是一个不错的解决方案。unique_ptr
这是一个显而易见的选择。std::shared_ptr
最后,您可以在 owner 对象中有一个,并传递给非所有者。std::shared_ptr
std::weak_ptr
评论
前言
您可能过度考虑了来自的额外间接的成本(除非您有很多这样的列表,并且您知道它们的使用会很频繁并且与其他过程混合在一起)。一般来说,我首先相信我的编译器会做一些聪明的事情。如果想知道成本,请进行性能分析。std::unique_ptr
在您的用例中,它的主要目的只是在引用它的其他数据被移动时,使用稳定的地址共享数据。如果在单个过程中多次使用长期对象的列表成员,则当您通过长期对象使用列表时,可以通过在过程的作用域中创建一个变量来帮助编译器(并获得一些更易于阅读的代码),该变量存储对 by like 所指向的引用:std::unique_ptr
std::list
std::unique_ptr
void fn(LongLiveHolder& holder) {
auto& list {holder.list.get()};
list.<some_operation_1>(...);
list.<some_operation_2>(...);
list.<some_operation_3>(...);
}
但同样,如果你真的想知道它会产生什么样的不同,你应该检查生成的机器代码并进行性能分析。
如果上下文允许,请编写自己的列表
你说:
但是,我真的不需要unique_ptr包装器。由于列表可能只包含指向第一个节点的 [unique_ptr] 指针(以及指向最后一个节点的指针),因此从理论上讲,我可以在其他对象上使用这些指针:[...]
考虑第一个节点的变化
如果允许删除列表的第一个节点,该怎么办?如果允许在列表的开头插入一个新节点,该怎么办?你需要一个非常具体的上下文,让这些不是要求。在短期对象中,您想要的是一个视图抽象,它支持与实际列表相同的接口,但只是不管理列表内容的生存期。如果将视图抽象实现为指向列表第一个节点的指针,那么视图对象将如何知道对“实际”/生存期管理列表视为第一个节点的内容的更改?它不能 - 除非生存期管理列表保留一个内部列表,其中包含其自身的所有活动视图,并且还更新这些视图(这本身就是性能和空间开销),即使那样,反之亦然呢?如果视图抽象用于更改第一个节点,则生存期管理列表将如何知道该更改?最简单、最明智的解决方案是增加一个间接级别:使视图指向列表,而不是创建视图时列表的第一个节点。
考虑获取列表大小的时间复杂度要求
我很确定不能只持有指向前后节点的指针。首先,由于 c++11 要求 std::list::size()
为 O(1),
因此可能必须始终在计数器成员中跟踪其大小 - 要么将其存储在自身中,要么在每个节点结构中进行某种大小跟踪,或者其他一些实现定义的行为。我敢肯定,将多个可移动引用(非常量指针)指向需要进行这种簿记的东西的最简单和最有效的方法是添加另一个间接级别。std::list
std::list
对于不需要该信息的特定情况,您可以尝试“跳过”簿记所需的间接层,这就是迭代器/节点指针方法,我稍后会对此进行评论。除了收藏本身之外,我想不出更好的地方或方法来存储簿记。即。如果列表接口具有需要此类簿记的要求,则为列表实现的每个用户提供额外的间接层具有非常强大的设计理由。
如果上下文允许
如果你不在乎有 O(1)
来获取列表的大小,并且你知道第一个节点在短期对象的生存期内不会改变,那么你可以编写自己的类列表视图类,并进行自己的特定于上下文的优化。这是像C++这样的语言的一大卖点:你会得到一个很好的标准库,它可以做通常有用的事情,当你有一个特定的场景,这些工具的某些功能是不需要的,并导致不必要的开销,你可以构建自己的工具/抽象(或者可能使用别人的库)。List
评论+参考std::unique_ptr
您的第一个代码片段有效,但您可以通过使用 std::reference_wrapper
获得一些更好的隐式构造函数等,因为当存在引用成员时,默认隐式声明的复制赋值和默认构造函数将被删除。SortLiveObject
class LongLiveHolder { std::unique_ptr<std::list<int>> list; };
class ShortLiveObject { std::reference_wrapper<std::list<int>> list; };
评注std::shared_ptr
+ std::weak_ref
就像 @Adrian Maire 建议的那样,在生存期较长的对象中,std::shared_ptr
可能会在生存期较短的对象存在时移动,而生存期较短的对象中的 std::weak_ptr
是一种可行的方法,但它可能比使用 + 引用有更多的开销(至少来自 ref-count),我想不出任何通用的优点,所以我不会建议它,除非您已经有其他理由使用 .在你给出的场景中,我很确定你不会。std::unique_ptr
std::shared_ptr
关于在短期对象中存储迭代器/节点指针的注释
@Daniel Langr 已经对此发表了评论,但我会尝试扩展。
具体来说,有一个可能符合标准的解决方案(有几个注意事项),它没有智能指针的额外间接性。警告:std::list
- 您必须可以只为生存期较短的对象(您表明您不是)提供迭代器接口。
- 前迭代器和后迭代器必须在生存期较短的对象的生存期内保持稳定。(不应从列表中删除迭代器,并且生存期较短的对象不会看到由使用生存期较长的对象的人推送到前面或后面的新列表条目)。
从 cppreference.com 的页面获取 std::list
的构造函数:
在容器移动构造(重载 (8))之后,对其他的引用、指针和迭代器(结束迭代器除外)仍然有效,但引用现在位于 中的元素。现行标准通过 [container.requirements.general]/12 中的一揽子声明做出此保证,并且正在考虑通过 LWG 2321 提供更直接的保证。
*this
来自 cppreference.com 的 std::list
页面:
在列表内或多个列表中添加、删除和移动元素不会使迭代器或引用失效。仅当删除相应的元素时,迭代器才会失效。
但我不是语言律师。我可能错过了一些重要的东西。
另外,你回复丹尼尔说:
某些迭代器在移动容器(例如insert_iterator)时无效@DanielLangr
是的,所以如果你想能够制作 s,请使用 + 引用方法并在需要时构造短期的 s,而不是试图存储长期的 s。std::input_iterator
std::unique_ptr
std::input_iterator
评论