将智能指针所有权转移给容器

4

我有一些数据结构通常使用 std::unique_ptr 进行管理,例如语法树中的表达式。

struct BinExpr {
 std::unique_ptr<Expr> left;    // Left owns the expression
 std::unique_ptr<Expr> right;   // Right owns the expression
};

在大多数情况下,它可以很好地工作。

但有时我没有固定数量的表达式,例如在列表中。

struct ListExpr {
  std::vector<std::unique_ptr<Expr>> exprs; // Exprs owns pointers which each own an expression 
};

但是我不喜欢通过vector中的智能指针进行额外的间接操作,而且我认为它并没有表达我想要的语义。
相比让智能指针拥有这些表达式,我认为vector应该拥有这些表达式。

但是我遇到了一个问题,那就是表达式总是在智能指针中创建(或者至少是作为原始指针):

std::unique_ptr<Expr> parse_expr() { ... }

有没有一种优雅的方法将类型为std::unique_ptr<Expr>parse_expr调用的所有权转移给std::vector<Expr>?当然,在这样做时Expr不能被复制。

std::vector<Expr> exprs;
exprs.push_back(move_from_ptr_to_vec(parse_expr()));

基本上,目前我的使用方式如下

std::vector<std::unique_ptr<Expr>> exprs;
exprs.push_back(std::move(parse_expr()));
return std::unique_ptr<ListExpr>(exprs);   // List has a std::vector<std::unique_ptr<Expr>>

但我想要那样

std::vector<Expr> exprs;
exprs.push_back(parse_expr());
return std::unique_ptr<ListExpr>(exprs);   // List has a std::vector<Expr>

这些AST Exprs是否属于具有虚拟方法的类层次结构? - Shawn
1
如果您的对象已经动态创建并由std::unique_ptr拥有,则将std::unique_ptr放在向量中是最好的选择。 将对象放入向量中而不进行复制或移动是不可能的。 - Slava
注意,这里移动操作是多余的,请不要到处放置它:exprs.push_back(std::move(parse_expr())); - Slava
1
但我不喜欢通过智能指针的这种额外间接性,这是“过早优化是万恶之源”的绝佳例子。 - Slava
如果你关心设计和语义,那么你需要首先问一下,为什么exprs()返回对象作为std_unique_ptr?在这里改变语义已经太晚了。 - Slava
显示剩余4条评论
2个回答

3
std::vector<Expr> exprs;
exprs.push_back(parse_expr());
由于parse_expr()返回智能指针,所以无法正常工作。您必须通过指针间接获取指向的对象。
exprs.push_back(*parse_expr());

在这样做时,Expr不得被复制。

然后移动,假设这是可以的:

exprs.push_back(std::move(*parse_expr()));

然而,要考虑为什么这些表达式首先会被动态分配。据推测,它们是指向多态基类子对象的指针。在这种情况下,从基类移动可能对您没有用,而整个基类对象数组的概念可能是错误的。

Expr 可以是一个基类。

在这种情况下,你需要使用 std::vector<std::unique_ptr<Expr>>


好的,我明白在我的当前情况下(其中Expr实际上是一个基类),我确实需要指针的向量。只是出于好奇,在move之前我不需要释放智能指针吗? - danielspaniol
1
@danielspaniol 不,你不应该释放智能指针。如果这样做,内存将会泄漏。只有在指针的所有权被转移(到除了独占指针之外的其他东西)时才进行释放。在这里,我们不拥有指向的对象,我们只是从该对象中移动。 - eerorika
不要忘记,移动操作实际上并没有移动动态分配的对象,也没有对其进行任何更改其生命周期的操作;它只是[可能]窃取该对象的内容。因此,您原来的unique_ptr仍然需要清理工作,只是在清理一个被移动过的(“死亡”)动态分配的对象。使用release将告诉它不要这样做,但这样会导致内存泄漏。如果您认为这很令人困惑,那是因为它确实如此! - Lightness Races in Orbit

-4

假设您的Expr是一个多态的,甚至是抽象的基类,简而言之:不行。 std::vector是用于具体类型的,如果您在其中存储纯指针,则立即受到n规则的限制,n>3。 您需要手动delete Expr,而目前std::unique_ptr会为您执行此操作。而且,创建更自动化的抽象类型容器几乎不值得努力。

顺便说一下,std::unique_ptr不拥有任何东西。其主要目的之一是让类拥有其成员,这些成员由于某种原因不能立即按值保留;成为抽象基类是相当合法的原因之一。请记住:您拥有您的对象,而不是您的指针;它所做的只是使您免除了基本管理您的对象的不必要负担。


6
“std::unique_ptr does not own anything”的意思是,unique_ptr并不真正拥有任何资源。然而,unique_ptr的确是为了拥有一个资源而存在的。 - François Andrieux
是的,它不管理生命周期,它的析构函数和其他方法会处理。 - bipll
3
我不明白为什么需要区分一个类型的作用和其成员函数(以及析构函数和构造函数)的作用。一个类型的行为是由其成员函数描述的,它的成员所做的就是该类型所做的。请问需要翻译其他的内容吗? - François Andrieux
2
你拥有你的对象,而不是指针;它只是让你从基本管理对象的不必要负担中解脱出来。在动态分配对象上调用delete是“拥有”该对象的定义,据我所知,还应该意味着什么才能“拥有”一个对象?! - 463035818_is_not_a_number
1
std::unique_ptr是一个智能指针,通过指针拥有和管理另一个对象,并在unique_ptr超出作用域时处理该对象。我不想排除您拥有“所有权”有效定义的可能性,但如果它与通常使用的定义差别很大,您应该解释一下它是什么 ;) - 463035818_is_not_a_number
按照这个逻辑,std::vector 也不拥有它的内容,因为这是由它的成员函数来处理的。 - Lightness Races in Orbit

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