采用这个微不足道的 class 来处理可选值可能会出什么问题?
What could possibly go wrong adopting this trivial class to handle optional values?
在某些应用程序中,我受困于 c++98 编译器。
我喜欢处理返回可选错误的想法,所以我设计了这个简单的 class 来处理它们:
template<class T> class Optional
{
public:
Optional<T>() : i_Assigned(false) {}
explicit Optional<T>(const T& v) : i_Value(v), i_Assigned(true) {}
bool assigned() const { return i_Assigned; }
const T& value() const { if(!i_Assigned) throw std::runtime_error("Optional value not assigned!"); return i_Value; }
Optional<T>& operator=(const T& v) { i_Value=v; i_Assigned=true; return *this; }
private:
T i_Value;
bool i_Assigned;
};
到目前为止,我对我的测试很满意,我即将在我的生产代码中使用它:
Optional<double> val = funct_returning_optional();
if( val.assigned() ) use(val.value());
else something_went_wrong();
...但它的极端简单让我想知道,我错过了什么?为什么标准实施如此复杂?采用我自制的可憎物品可能会出什么问题?
您的实现的一个基本问题是它无法适应缺少默认构造函数的类型。同样,如果 T
的默认构造函数有任何副作用,那么在构造 Optional<T>
时也会产生这些副作用。 std::optional<T>
在默认构造时不应该有任何副作用(事实上,默认构造函数是 constexpr
)。
std::optional
处理此问题的方法是不使用 T
类型的成员,而是使用一个联合成员,其中一个元素的类型为 T
:
template <class T>
class optional {
union Storage {
T t;
char c;
Storage() : c(0) {}
} storage;
bool has_value;
public:
optional() : has_value(false) {}
};
默认构造可选对象时,初始化了storage.c
成员,还没有T
对象,所以T
不需要调用构造函数。
这立即使实施的其余部分复杂化。每当可选项从具有 T
变为不具有 T
时(当它被销毁、重置或从空可选项分配时可能发生),必须在 [=23 上显式调用析构函数=].每当它从没有 T
到有 T
时(当它是从非空可选的复制构造,从 T
分配或从非空分配时可能发生-empty optional), placement new 必须用于开始 storage.t
.
的生命周期
C++17 optional
还有其他一些关键要求。我假设您已经了解移动构造函数和移动赋值运算符。然而,真正使事情复杂化的是,只要 T
的相应成员是微不足道的,std::optional<T>
就需要有一个微不足道的复制构造函数、复制赋值运算符、移动构造函数、移动赋值运算符或析构函数。在 C++17 中,实现此要求的唯一方法是让 optional<T>
从内部基类 class 中引入其特殊成员函数,其中一个版本的基类 class 处理通过使用隐式定义的函数处理普通情况,另一个版本使用用户提供的函数处理非普通情况。这就是标准库实现这么长的主要原因。
在某些应用程序中,我受困于 c++98 编译器。 我喜欢处理返回可选错误的想法,所以我设计了这个简单的 class 来处理它们:
template<class T> class Optional
{
public:
Optional<T>() : i_Assigned(false) {}
explicit Optional<T>(const T& v) : i_Value(v), i_Assigned(true) {}
bool assigned() const { return i_Assigned; }
const T& value() const { if(!i_Assigned) throw std::runtime_error("Optional value not assigned!"); return i_Value; }
Optional<T>& operator=(const T& v) { i_Value=v; i_Assigned=true; return *this; }
private:
T i_Value;
bool i_Assigned;
};
到目前为止,我对我的测试很满意,我即将在我的生产代码中使用它:
Optional<double> val = funct_returning_optional();
if( val.assigned() ) use(val.value());
else something_went_wrong();
...但它的极端简单让我想知道,我错过了什么?为什么标准实施如此复杂?采用我自制的可憎物品可能会出什么问题?
您的实现的一个基本问题是它无法适应缺少默认构造函数的类型。同样,如果 T
的默认构造函数有任何副作用,那么在构造 Optional<T>
时也会产生这些副作用。 std::optional<T>
在默认构造时不应该有任何副作用(事实上,默认构造函数是 constexpr
)。
std::optional
处理此问题的方法是不使用 T
类型的成员,而是使用一个联合成员,其中一个元素的类型为 T
:
template <class T>
class optional {
union Storage {
T t;
char c;
Storage() : c(0) {}
} storage;
bool has_value;
public:
optional() : has_value(false) {}
};
默认构造可选对象时,初始化了storage.c
成员,还没有T
对象,所以T
不需要调用构造函数。
这立即使实施的其余部分复杂化。每当可选项从具有 T
变为不具有 T
时(当它被销毁、重置或从空可选项分配时可能发生),必须在 [=23 上显式调用析构函数=].每当它从没有 T
到有 T
时(当它是从非空可选的复制构造,从 T
分配或从非空分配时可能发生-empty optional), placement new 必须用于开始 storage.t
.
C++17 optional
还有其他一些关键要求。我假设您已经了解移动构造函数和移动赋值运算符。然而,真正使事情复杂化的是,只要 T
的相应成员是微不足道的,std::optional<T>
就需要有一个微不足道的复制构造函数、复制赋值运算符、移动构造函数、移动赋值运算符或析构函数。在 C++17 中,实现此要求的唯一方法是让 optional<T>
从内部基类 class 中引入其特殊成员函数,其中一个版本的基类 class 处理通过使用隐式定义的函数处理普通情况,另一个版本使用用户提供的函数处理非普通情况。这就是标准库实现这么长的主要原因。