混合 boost::optional 和 std::unique_ptr
Mix boost::optional and std::unique_ptr
我承认:我爱上了可选的概念。自从我发现它以来,我的代码质量有了很大的提高。明确变量是否有效比简单的错误代码和带内信号要好得多。它还让我不必担心必须阅读文档中的合同,或担心它是否是最新的:代码本身就是合同。
也就是说,有时我需要处理 std::unique_ptr
。这种类型的对象可能为空,也可能不是;在代码中的给定点不可能知道 std::unique_ptr
是否应该有值;不可能从代码中知道合约。
我想以某种方式混合optional
(可能与boost::optional
)和std::unique_ptr
,这样我就有了一个动态分配的对象,它具有范围破坏和正确的 copy/move 行为,明确声明它可能没有值 。这样,我可以使用这种新类型来明确表示必须检查值,并避免对普通 std::unique_ptr
.
进行不必要的检查
在 C++11 标准中是否有用于此的工具,boost
或足够流行的库?我可以接受为此定义我自己的 class,但这是最不受欢迎的方法(由于缺乏彻底的测试)。
因此,为了概括您的问题,您需要:
- 由value/on堆栈分配的非可选类型:您很高兴直接为此使用对象类型。
- 由 value/on 堆栈分配的可选类型:您很高兴为此使用
boost::optional
(或者您可以使用 C++17 中的 std::optional
)。
- 在堆上分配并拥有指向对象的非可选类型。
- 在堆上分配并拥有指向对象的可选类型。
你很不高兴你可以表达1和2的区别,但是3和4通常使用相同的类型(std::unique_ptr
)。您建议对 3 使用 std::unique_ptr
,绝不允许 nullptr
,对 4 使用其他一些东西,但想知道您可以使用什么。 (在评论中,如果可以为 3 找到其他东西,您也可以接受将 std::unique_ptr
和 nullptr
用于 4 的可能性。)
您问题的字面答案:您可以简单地使用 boost::optional<std::unique_ptr<T>>
表示 4(而按照您的建议使用裸 unique_ptr
表示 3)。
您问题的替代文字答案: 正如@StoryTeller 所说,您可以定义自己的智能指针类型,类似于 unique_ptr
但不允许 nullptr
,并将其用于 3。更快(但非常肮脏)的替代方法是强制函数对 unique_ptr
和对同一对象的引用的 return a pair
。然后只通过引用访问结果,但只有在 unique_ptr
仍然存在时才这样做:
template<class T>
using RefAndPtr = std::pair<T&, std::unique_ptr<T>>;
RefAndPtr<Foo> getFoo()
{
std::unique_ptr<Foo> result = std::make_unique<Foo>();
return RefAndPtr<Foo>(*result, std::move(result));
}
我的实际建议: 接受并为 3 和 4 使用 std::unique_ptr
。在类型系统中阐明你的意图是一件好事,但也许多好事可能是坏事。使用上述任一选项只会让阅读您的代码的任何人感到困惑。即使您阻止人们错误地传递 nullptr
,如何阻止他们传递指向错误对象或已释放内存等的指针?在某些时候,您必须指定类型系统之外的内容。
std::unique_ptr
可以为空。每当移出或默认构造时,它都会变为 null。
std::unique_ptr
是您的可空堆分配对象。
一个value_ptr
可以写成不可为空的。注意搬家有额外费用:
template<class T>
class value_ptr {
struct ctor_key_token{ explicit ctor_key_token(int){} };
public:
template<class A0, class...Args, class dA0 = std::decay_t<A0>,
std::enable_if_t<!std::is_same<dA0, ctor_key_token>{} && !std::is_same<dA0, value_ptr>{}, int> = 0
>
value_ptr( A0&& a0, Args&&... args):
value_ptr( ctor_key_token(0), std::forward<A0>(a0), std::forward<Args>(args)... )
{}
value_ptr(): value_ptr( ctor_key_token(0) ) {}
template<class X, class...Args>
value_ptr( std::initializer_list<X> il, Args&&... args ):
value_ptr( ctor_key_token(0), il, std::forward<Args>(args)... )
{}
value_ptr( value_ptr const& o ):
value_ptr( ctor_key_token(0), *o.state )
{}
value_ptr( value_ptr&& o ):
value_ptr( ctor_key_token(0), std::move(*o.state) )
{}
value_ptr& operator=(value_ptr const& o) {
*state = *o.state;
return *this;
}
value_ptr& operator=(value_ptr && o) {
*state = std::move(*o.state);
return *this;
}
T* get() const { return state.get(); }
T* operator->() const { return get(); }
T& operator*() const { return *state; }
template<class U,
std::enable_if_t<std::is_convertible<T const&, U>{}, int> =0
>
operator value_ptr<U>() const& {
return {*state};
}
template<class U,
std::enable_if_t<std::is_convertible<T&&, U>{}, int> =0
>
operator value_ptr<U>() && {
return {std::move(*state)};
}
private:
template<class...Args>
value_ptr( ctor_key_token, Args&&... args):
state( std::make_unique<T>(std::forward<Args>(args)...) )
{}
std::unique_ptr<T> state;
};
这是不可为空的堆分配值语义对象的粗略草图。
请注意,当您从它移动时,它不会释放旧内存。它唯一不拥有堆上的 T
的时间是在构造期间(只能通过抛出中止)和销毁期间(因为 state
被销毁)。
更高级的版本可以有自定义的破坏者、克隆者和移动者,允许存储多态值语义类型或不可复制的类型。
使用几乎从不为空或很少为空的类型作为从不为空会导致错误。所以不要这样做。
在 C++ 的类型系统中,编写不可为 null 的 unique_ptr
是不可能的。 unique_ptr
可为空 而不是 只是惯例。这是许多评论中严重遗漏的要点。移动构造函数会是什么样子?这一点之前已经讲过:https://youtu.be/zgOF4NrQllo?t=38m45s。由于不可为空的 unique_ptr
类型是不可能的,因此在任何一种情况下您都可以使用 unique_ptr
指针。
如果需要,您可以创建一个类似于 unique_ptr
的指针类型,但没有 public 默认构造函数。每次移动时它仍会进入空状态。这并没有给你太多保证,但它给了你一点,它可以作为文档。我认为这种类型的价值不足以证明它的存在。
我建议简单地在代码库中制定一个约定,即 std::unique_ptr
总是指向某些东西 除非 它是在构造函数中初始化的 class 成员或者刚刚被取消引用并且即将超出范围(并且只有在这些情况下它可能包含 null)
我承认:我爱上了可选的概念。自从我发现它以来,我的代码质量有了很大的提高。明确变量是否有效比简单的错误代码和带内信号要好得多。它还让我不必担心必须阅读文档中的合同,或担心它是否是最新的:代码本身就是合同。
也就是说,有时我需要处理 std::unique_ptr
。这种类型的对象可能为空,也可能不是;在代码中的给定点不可能知道 std::unique_ptr
是否应该有值;不可能从代码中知道合约。
我想以某种方式混合optional
(可能与boost::optional
)和std::unique_ptr
,这样我就有了一个动态分配的对象,它具有范围破坏和正确的 copy/move 行为,明确声明它可能没有值 。这样,我可以使用这种新类型来明确表示必须检查值,并避免对普通 std::unique_ptr
.
在 C++11 标准中是否有用于此的工具,boost
或足够流行的库?我可以接受为此定义我自己的 class,但这是最不受欢迎的方法(由于缺乏彻底的测试)。
因此,为了概括您的问题,您需要:
- 由value/on堆栈分配的非可选类型:您很高兴直接为此使用对象类型。
- 由 value/on 堆栈分配的可选类型:您很高兴为此使用
boost::optional
(或者您可以使用 C++17 中的std::optional
)。 - 在堆上分配并拥有指向对象的非可选类型。
- 在堆上分配并拥有指向对象的可选类型。
你很不高兴你可以表达1和2的区别,但是3和4通常使用相同的类型(std::unique_ptr
)。您建议对 3 使用 std::unique_ptr
,绝不允许 nullptr
,对 4 使用其他一些东西,但想知道您可以使用什么。 (在评论中,如果可以为 3 找到其他东西,您也可以接受将 std::unique_ptr
和 nullptr
用于 4 的可能性。)
您问题的字面答案:您可以简单地使用 boost::optional<std::unique_ptr<T>>
表示 4(而按照您的建议使用裸 unique_ptr
表示 3)。
您问题的替代文字答案: 正如@StoryTeller 所说,您可以定义自己的智能指针类型,类似于 unique_ptr
但不允许 nullptr
,并将其用于 3。更快(但非常肮脏)的替代方法是强制函数对 unique_ptr
和对同一对象的引用的 return a pair
。然后只通过引用访问结果,但只有在 unique_ptr
仍然存在时才这样做:
template<class T>
using RefAndPtr = std::pair<T&, std::unique_ptr<T>>;
RefAndPtr<Foo> getFoo()
{
std::unique_ptr<Foo> result = std::make_unique<Foo>();
return RefAndPtr<Foo>(*result, std::move(result));
}
我的实际建议: 接受并为 3 和 4 使用 std::unique_ptr
。在类型系统中阐明你的意图是一件好事,但也许多好事可能是坏事。使用上述任一选项只会让阅读您的代码的任何人感到困惑。即使您阻止人们错误地传递 nullptr
,如何阻止他们传递指向错误对象或已释放内存等的指针?在某些时候,您必须指定类型系统之外的内容。
std::unique_ptr
可以为空。每当移出或默认构造时,它都会变为 null。
std::unique_ptr
是您的可空堆分配对象。
一个value_ptr
可以写成不可为空的。注意搬家有额外费用:
template<class T>
class value_ptr {
struct ctor_key_token{ explicit ctor_key_token(int){} };
public:
template<class A0, class...Args, class dA0 = std::decay_t<A0>,
std::enable_if_t<!std::is_same<dA0, ctor_key_token>{} && !std::is_same<dA0, value_ptr>{}, int> = 0
>
value_ptr( A0&& a0, Args&&... args):
value_ptr( ctor_key_token(0), std::forward<A0>(a0), std::forward<Args>(args)... )
{}
value_ptr(): value_ptr( ctor_key_token(0) ) {}
template<class X, class...Args>
value_ptr( std::initializer_list<X> il, Args&&... args ):
value_ptr( ctor_key_token(0), il, std::forward<Args>(args)... )
{}
value_ptr( value_ptr const& o ):
value_ptr( ctor_key_token(0), *o.state )
{}
value_ptr( value_ptr&& o ):
value_ptr( ctor_key_token(0), std::move(*o.state) )
{}
value_ptr& operator=(value_ptr const& o) {
*state = *o.state;
return *this;
}
value_ptr& operator=(value_ptr && o) {
*state = std::move(*o.state);
return *this;
}
T* get() const { return state.get(); }
T* operator->() const { return get(); }
T& operator*() const { return *state; }
template<class U,
std::enable_if_t<std::is_convertible<T const&, U>{}, int> =0
>
operator value_ptr<U>() const& {
return {*state};
}
template<class U,
std::enable_if_t<std::is_convertible<T&&, U>{}, int> =0
>
operator value_ptr<U>() && {
return {std::move(*state)};
}
private:
template<class...Args>
value_ptr( ctor_key_token, Args&&... args):
state( std::make_unique<T>(std::forward<Args>(args)...) )
{}
std::unique_ptr<T> state;
};
这是不可为空的堆分配值语义对象的粗略草图。
请注意,当您从它移动时,它不会释放旧内存。它唯一不拥有堆上的 T
的时间是在构造期间(只能通过抛出中止)和销毁期间(因为 state
被销毁)。
更高级的版本可以有自定义的破坏者、克隆者和移动者,允许存储多态值语义类型或不可复制的类型。
使用几乎从不为空或很少为空的类型作为从不为空会导致错误。所以不要这样做。
在 C++ 的类型系统中,编写不可为 null 的 unique_ptr
是不可能的。 unique_ptr
可为空 而不是 只是惯例。这是许多评论中严重遗漏的要点。移动构造函数会是什么样子?这一点之前已经讲过:https://youtu.be/zgOF4NrQllo?t=38m45s。由于不可为空的 unique_ptr
类型是不可能的,因此在任何一种情况下您都可以使用 unique_ptr
指针。
如果需要,您可以创建一个类似于 unique_ptr
的指针类型,但没有 public 默认构造函数。每次移动时它仍会进入空状态。这并没有给你太多保证,但它给了你一点,它可以作为文档。我认为这种类型的价值不足以证明它的存在。
我建议简单地在代码库中制定一个约定,即 std::unique_ptr
总是指向某些东西 除非 它是在构造函数中初始化的 class 成员或者刚刚被取消引用并且即将超出范围(并且只有在这些情况下它可能包含 null)