boost::optional
13 May 20211. 问题
可选值(optional value),是这样的一些值:他们可以存在,也可以不存在。存在很多这样的例子,例如:中间名(不是每个人都有),vector的最小值(当vector为空时不存在)或者队列中最后一个未处理的命令。
boost::optional库就是为了这种情况设计的。
我将给出处理可选值的可能实现,首先不使用boost::optional,然后使用boost::optional。下面的例子中,Pool类可以作为对象Object的容器,它可以表示应处理的命令。Pool中的最重要的对象总是最后一个,因此Pool接口应该提供获取容器中最后一个对象的方法getLastObject
。由于Object较小,可以接受按值返回的对象。
struct object {
object() : m_value(0);
int m_value;
}
class Pool {
public:
Pool() {};
object getLastObject() {
// 在这里,m_data 有可能为空
return m_data.back();
}
private:
std::vector<object> m_data;
};
问题是在getLastObject的实现中出现的。当vector为空时,我们需要确保正确的行为。但是如何设计return的内容呢?
2. Without boost::optional
2.1 call getLastObj carefully[anti-pattern!]
实现Pool::isEmpty
方法。仅当Pool不为空时才调用getLastObject
。
// pool 为 Pool 的实例化对象
if (!pool.empty()) {
object = pool.getLastObject();
}
else {
std::cout << "The pool was empty!";
}
这是一种考验类的使用者自觉性的写法,如果你认为这是最糟糕的实现,那么你是对的。接口应该是容错的。当你忘记做正确的检查或别人在不知道限制的情况下尝试使用它时,它会导致未定义的行为。
根据Scott Meyers的说法,这种实现显然违反了最重要的设计指导原则:
Make interfaces easy to use correctly and hard to use incorrectly.
忘了这种信任用户的糟糕设计吧!
2.2 Return invalid-object value
如果vector为空,则返回Object的特定无效值。
Object Pool::getLastObject()
{
if (!m_data.empty()) {
return m_data.back();
}
else {
return Object();
}
}
else中返回的结果可以是默认值或任何其他值(负的,极端的),只要它不可能是正常的对象出现的值。否则,将无法区分正常对象和非法对象。
Pool pool;
Object object = pool.getLastObject();
std::cout << "The last Object's value is: " << obj.m_value << std::endl;
// The last Object's value is: 0
// But wait, Pool was empty...
这样的代码可能会产生误导,它很容易忘记检查对象的值是否有效,并将非法对象视为正常对象。而且,并不总是可能使用这样的实现。一些对象需要完整的值范围,对于它们没有特定的不存在的非法值。
这种方法也有性能缺陷,我们需要调用构造函数,尽管我们并不真正需要这个非法对象。这里的假设对象很小,构造函数代价较少,无论如何,这里的调用是完全多余的。
2.3 Return bool value
返回值表示Object的值是否有效。Object是通过其引用作为函数的参数返回的。
bool getLastObject(object& object) {
if (!m_data.empty()) {
object = m_data.back();
return true;
} else {
return false;
}
}
Pool pool;
Object object;
bool retVal = pool.getLastObject(object);
if (retVal) {
std::cout << "The last value is: " << object.m_value << std::endl;
} else {
std::cout << "The pool was empty!" << std::endl;
}
这种方法看起来不错,但是还是不够完美。当retValue与Object分开时(例如,将Object传递给无法访问retValue的其他函数),也就无法判断该Object是否有效。让是否非法信息与Object对象耦合可能是更好的主意。这种效果可以通过几种方式实现:使用bool和Object组成一个pair,将bool成员添加到Object结构体中,或像下一节一样,使用指向Object的指针处理,其中nullptr表示vector中不存在值。
2.4 Use nullptr for not existing object
Pool将存储指向对象的指针的vector,而不是存储对象的vector。
class Pool
{
public:
Pool() {}
Object* getLastObject() {
if (!m_data.empty())
{
return m_data.back();
}
else {
return nullptr;
}
}
private:
std::vector<Object*> m_data;
};
Pool pool;
Object* object = pool.getLastObject();
if (object != nullptr) {
std::cout << "The last value is: " << object->m_value << std::endl;
}
else {
std::cout << "The pool was empty!" << std::endl;
}
无法将nullptr与已经存在的对象进行匹配,仍然可以使用完整范围的m_value成员,并且无需在Object结构中添加其他成员。还避免了不必要的构造函数的开销。不管怎样,使用原始指针听起来像是自找麻烦。需要注意指针的重新分配和处理空引用。
上面的就是各种各样的解决方案啦,那么如何使用boost::optional呢?
3. With boost::optional
3.1 Setup
包含下列头文件:
#include <boost/optional/optional.hpp>
using boost::optional;
或者使用标准库的experimental:
#include <experimental/optional>
using std::experimental::optional;
3.2 Usage
模板类boost::optional是一个对象的包装,该对象可以具有有效值,也可以具有无效值。 可选对象的构造如下所示:
Object someObject;
optional<Object> object_initialized = someObject;
// copy constructor of `Object` invoked
optional<Object> object_uninitialized;
// doesn't call the `Object` constructor
对于未初始化的optional<Object>
,不调用Object的构造函数。
然后就可以随时检查对象是否已初始化。这可以is_initialized
函数或简单地将对象放入条件表达式来完成。optional<Object>
将转换为bool类型。
// equal to object.is_initialized
if (object) {
// The object was initialized.
} else {
// The object was not initialized.
}
使用boost::optional
的Pool::getLastObject
的实现如下所示。返回类型为optional<Object>
,如果vector非空,optional<Object>
被正确的值初始化。否则返回optional<object>
类型的空对象。
optional<Object> Pool::getLastObject()
{
if (!m_data.empty()) {
return m_data.back();
} else {
return optional<Object>{};
}
}
上面的代码具有等效的简短形式:
optional<Object> Pool::getLastObject() {
return !m_data.empty() ? m_data.back() : optional<Object>{};
}
现在我们可以用下面的代码从Pool中获得最后一个对象。使用->操作符从optional<Object>
访问m_value。
Pool pool;
optional<Object> object = pool.getLastObject();
if (object) {
std::cout << "The last value is: " << object->m_value << std::endl;
} else {
std:: cout << "The pool was empty!" << std::endl;
}
你也可以使用get()方法从optional<Object>
接收Object
类型的实例。
std::cout << "The last value is: " << object.get().m_value << std::endl;
如果对象未初始化,则调用get()方法或->,*操作符将导致一个断言。
3.3 Conditional constructor
还有另一种构造可选对象的方法,使用特殊的双参数构造函数:optional<T>{conditiaon, value}
,第一个参数是一个条件,第二个条件是一个初始化值,当条件为真的时候被使用。当条件为假时,对象保持未初始化。
当初始化值已经存在或是一个内置类型(如int)时,此构造函数非常有用:
optional<int> getValue() {
return optional<int>{condition == true, 5};
}
4. Summary
- Boost.optional设计用于可以初始化和未初始化的值,这两种情况都很常见;
- Boost.optional并没有对对象可以使用的可能值的设置限制;
- Boost.optional比使用nullptr的方法更安全;
- Boost.optional不会为未初始化的值调用被包装类型构造函数;
- Boost.optional提供了有关对象状态(已初始化/未初始化)的易于访问和直接的信息;
- Boost.optional使您的代码美观,安全且易于调试。