使用 std::move() 从 null 时无效的类中提取 std::unique_ptr 成员:如何避免无形中使对象无效?

问题描述 投票:0回答:1

考虑以下场景:

struct foo {};

class bar
{
public:
    bar(std::unique_ptr<foo> data) : data{ std::move(data) } {}

    foo* get_data() { return (this->data).get(); }
private:
    std::unique_ptr<foo> data;
};

class baz
{
public:
    baz() = default;

    void add_data(std::unique_ptr<foo> data) { vec.push_back(std::move(data)); }
private:
    std::vector<std::unique_ptr<foo>> vec;
};

int main()
{
    std::unique_ptr<foo> data_0{ std::make_unique<foo>() };
    bar bar_0{ std::move(data_0) };

    std::unique_ptr<foo> data_1{ std::make_unique<foo>() };
    std::unique_ptr<foo> data_2{ std::make_unique<foo>() };
    std::unique_ptr<foo> data_3{ std::make_unique<foo>() };
    baz baz_0;
    baz_0.add_data(std::move(data_1));
    baz_0.add_data(std::move(data_2));
    baz_0.add_data(std::move(data_3));
}

我们有一个类

bar
,它通过唯一的指针拥有一个
foo
数据对象。
bar
必须始终可构造成有效状态;也就是说,
bar
必须始终拥有一些数据,因此
data
永远不应该为空。我们可以调用
get_data()
来查看
bar
的数据,但只要
bar
还活着,该数据的所有权就不会改变。因此,当我们构造它时,数据被添加到
bar
:构造函数通过移动语义接收唯一的指针。

然后我们有

baz
,它可以拥有多个
foo
数据。我们可以使用
add_data()
一次添加每个数据,使用
std::move()
将数据移动到
baz
中。到目前为止,一切都很好。

现在我希望能够将

bar
中的数据合并到
baz
中:

void merge_data(baz& target, bar&& source)
{
    // ...
}

与提供的示例代码相反,这些类可能代表大型数据结构,在这种情况下最好避免不必要的复制。此函数的目的是通过移动语义接收

bar
,并且存储在其上的任何数据现在都会传输到
baz

问题是,这需要从

bar
中提取数据及其所有权。这意味着将
bar
置于无效的空状态。如果此提取发生在
merge_data()
内部,则没问题,因为
bar
已移入,因此生成的无效
bar
不应该出现。然而,提取此数据将涉及添加一个可访问的成员函数
merge_data()
。因此,我考虑将以下公共成员添加到
bar

std::unique_ptr<foo> extract_data() { return std::move(this->data); }

这没问题,但是有一个问题:由于这是公开的,这意味着存在可能在

foo
的生命周期中调用它的风险,从而冒着
foo
的可能性使用过程中出现无效的空状态。我可能知道调用此函数会留下无效的
bar
,并记住在执行此操作后不要使用
bar
。但我怀疑这对于其他看到该函数并认为提取的数据只是从
bar
复制的用户来说将是一个不幸的惊喜!

如何允许此

extract_data()
操作,同时避免留下毫无戒心的无效
bar
对象?

c++ unique-ptr move-semantics ownership-semantics
1个回答
0
投票

一种方法是使其成为私人会员,并添加

merge_data()
为好友。从可管理性的角度来看,这可能不是最理想的方法,特别是如果我想将数据合并到其他类时;我需要为每个用例添加一个朋友声明。

考虑到只有在不可能留下无效的

extract_data()
的情况下才应该调用
bar
,我尝试过的一种方法是使用右值引用限定符,这样
extract_data()
只能在以下情况下被调用:
bar
是右值。因此,对于一个完整的例子:

struct foo {};

class bar
{
public:
    bar(std::unique_ptr<foo> data) : data{ std::move(data) } {}

    foo* get_data() { return (this->data).get(); }
    std::unique_ptr<foo> extract_data() && { return std::move(this->data); }
private:
    std::unique_ptr<foo> data;
};

class baz
{
public:
    baz() = default;

    void add_data(std::unique_ptr<foo> data) { vec.push_back(std::move(data)); }
private:
    std::vector<std::unique_ptr<foo>> vec;
};

void merge_data(baz& target, bar&& source)
{
    target.add_data(std::move(source).extract_data());
}

int main()
{
    bar bar_0{ std::make_unique<foo>() };

    baz baz_0;
    merge_data(baz_0, std::move(bar_0));

    bar bar_1{ std::make_unique<foo>() };
    std::unique_ptr<foo> extracted{ std::move(bar_1).extract_data() };
}

这样,每当从

bar
提取数据时,用户都必须在
std::move()
的任何左值引用上使用
bar
。我意识到这并不能真正解决防止现有无效
bar
的问题:
bar_1
可能在移动后无效。但在处理
std::move()
时,情况总是如此。由于我们被迫在我们想要从中提取数据的任何左值
std::move()
上使用
bar
,因此向用户明确表示,事后使用
bar
的任何使用都将被取消。

© www.soinside.com 2019 - 2024. All rights reserved.