考虑以下场景:
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
对象?
一种方法是使其成为私人会员,并添加
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
的任何使用都将被取消。