编程中,我们经常会需要表示或处理一个“可能为空”的变量,可能是一个为包含任何元素的容器,可能是一个类型的指针没有指向任何有效的对象实例,再或者是一个对象没有被赋予有效的值。通常处理这类问题意味着写更多的代码来处理这些“特殊”情况,很容易导致代码变得冗余,可读性变差或者容易出错。比如,我们很容易想到的如下三种方法:
bool function(tResult & result);
C++17中的std::optional
template
class optional
{
bool _initialized;
std::aligned_storage_t _storage;
public:
// operations
};
使用std::optional我们可以写出如下的代码:
std::optional tItem::findShortName()
{
if (hasShortName)
{
return mShortName;
}
return std::nullopt;
}
// 使用
std::optional shortName = item->findShortName();
if (shortName)
{
PRITNT(*shortName);
}
在上面的例子中,我们定义了函数findShortName返回一个包含字符串类型的optional对象。如果商品(tItem)有缩写名称,则返回缩写,否则将返回nullopt表示缩写名称为空。optional类型可以隐式转换为boolean类型来表示当前是否有有效值,同时optional支持操作符*来进行取值。从这个例子中我们可以看出来,用optional作为函数返回值可以更好地解决引言中的问题,函数更简洁同时接口含义也更明确。
如以下代码所示:
//初始化为空
std::optional emptyInt;
std::optional emptyDouble = std::nullopt;
//直接用有效值初始化
std::optional intOpt{10};
std::optional intOptDeduced{10.0}; // auto deduced
//使用make_optional
auto doubleOpt = std::make_optional(10.0);
auto complexOpt = std::make_optional>(3.0, 4.0);
//使用in_place
std::optional> complexOpt{std::in_place, 3.0, 4.0};
std::optional> vectorOpt{std::in_place, {1, 2, 3}};
//使用其它optional对象构造
auto optCopied = vectorOpt;
std::optional的其中一个构造函数接受U&&(U为可转换为optional底层类型的类型)作为参数进行构造。
template
constexpr optional(U&& value);
因此对于可以转换为optional底层类型的类型,我们可以将其直接传入optional构造函数,从而节省了一次临时对象的构造和拷贝。
std::optional strOpt {"hello world"};
那为何我们还需要in_place / make_optional来对optional对象进行“原地”构造呢?主要是考虑到如下3种情形:
如果我们有这样一个类,它提供一个默认构造函数如下:
class tSampleClass
{
public:
tSampleClass() : mInt(100)
{
}
};
如果我们想用默认函数构造的tSampleClass对象构造optional,代码应该怎么写呢?
你可能会想到如下写法:
std::optional sample;
std::optional sample{};
但是这两种方法得到的结果都只是空的optional对象,而不是包含默认构造值的对象。
你还可以这么写:
std::optional sample{tSampleClass()};
这种方法是可以工作的,我们将得到包含默认tSampleClass对象的optional对象。但是在上面的代码中,将先构造出一个tSampleClass的临时对象,然后调用move函数将这个临时对象“移动”到optional存储的对象中,带来了额外的开销。在这种情况下,我们就可以使用std::in_place_t / std::make_optional来“原地”构造optional底层存储的对象。
std::optional opt{std::in_place};
auto opt = std::make_optional();
此时opt存储的tSampleClass是被“原地”构造出来的,不会引入额外的copy或者move。
假如我们的tSampleClass类型不支持移动和拷贝:
class tSampleClass
{
public:
tSampleClass() : mInt(100)
{
}
tSampleClass(const tSampleClass &) = delete;
tSampleClass & operator= (const tSampleClass &) = delete;
tSampleClass(tSampleClass &&) = delete;
tSampleClass & operator= (tSampleClass &&) = delete;
};
在上面的例子中我们看到,如果使用一个临时的对象来初始化optional,那么会调用移动或者拷贝构造函数。显而易见,对于上述移动和拷贝构造函数被禁用的类型(如:std::mutex),我们就只能使用std::in_palce来初始化optional了。
当构造函数有个多个参数时,也推荐使用原地构造的方法来提高效率。optional提供如下构造函数来处理多参数原地构造的情况:
template
constexpr explicit optional(std::in_place_t, tArgs&&... args);
template
constexpr explicit optional(std::in_place_t, std::initializer_list list, tArgs&&... args);
若我们需要够造一个多参数optional,代码可以写成如下形式:
std::optional> complexOpt(std::in_place, 3.0, 4.0); //第一个构造函数
std::optional> vectOpt(std::in_place, {1, 2, 3}); //第二个构造函数
auto complexOpt = std::make_optional>(3.0, 4.0);
auto vectOpt = std::make_optional>({1, 2, 3});
如果用optional对象作为函数返回值,那么我们将很容易地解决引言中所述的问题。如果函数失败,则返回std::nullopt表示没有有效返回值,否则就直接返回计算值。这样代码将更将简介,可靠。
std::optional findStudent(const std::map & students, const std::string & name)
{
if (students.find(name) == students.end())
{
return std::nullopt;
}
return students[name];
}
//使用
auto studentId = findStudent(students, "Bob");
C++17引入了guaranteed copy illision,上例中的optional对象在调用处构造。
说到函数返回值,有一个有趣的问题值得讨论,先来看如下代码:
std::optional createString()
{
std::string result{"Hello world!"};
return {result}; //产生拷贝
// return result; //只产生move
}
根据C++标准,在函数体内部的临时变量作为函数返回值时,临时变量将被move到目标变量中,而不是被copy过去。但是当我们用{}将变量名括起来时,临时变量将强制被copy而不是被move。对于non-copyable的类型,如std::unique_ptr,见如下例子:
std::unique_ptr nonCopyableReturn()
{
std::unique_ptr p = nullptr;
return {p}; //强制产生拷贝,将产生编译错误,因为unique_ptr为non-copyable类型
// return p; //move语义,unique_ptr可以move,编译通过。
}
// operator* 和 operator->
// operator* 返回内部存储对象的引用,operator->返回指向内部存储对象的指针
// 如果没有有效值,则行为未定义
std::optional opt{"abc"};
std::cout << "content is " << *opt << ", size is " << opt->size() << std::endl;
// value()
// 返回内部存储对象的值,当optional为空时抛出std::bad_optional_access异常
try
{
std::cout << "content is " << opt.value() << std::endl;
}
catch(const std::bad_optional_access & e)
{
std::cout << e.what() << std::endl;
}
// value_or(defaultValue)
// optional有有效值时返回有效值,否则返回默认值
std::optional optInt(100);
std::cout << "value is " << optInt.value_or(10) << std::endl;
对于一个已经存在的optional对象,通过调用emplace, reset, swap, operator=,可以将其中存储的值修改掉。如果调用operator=或者reset将optional对象赋值为nullopt,若之前的optional存储有有效值,则存储类型的析构函数将被调用。除此之外,每次optional内部存储对象被重置,之前对象的析构函数都会被调用。
class tStudent
{
public:
explicit tStudent(std::string str)
: m_name(str)
{}
~tStudent() = default;
};
// 构造空的optional
std::optional optStudent;
// 构造名字为“Bob”的tStudent对象存储在optional对象中
optStudent.emplace("Bob");
// 相当于
// optStudent = tStudent{"Bob"};
// "Bob"对象析构,构造"Steve"
optStudent.emplace("Steve")
// "Steve"对象析构
optStudent.reset();
对于定义了<,>,==操作符的类型,保存他们的optional对象也可以比较大小。如optional
std::optional int1(1);
std::optional int2(10);
std::optional int3;
std::cout << std::boolalpha;
std::cout << (int1 < int2) << std::endl; // true
std::cout << (int2 > int1) << std::endl; // true
std::cout << (int3 == std::nullopt) << std::endl; // true
std::cout << (int3 < int1) << std::endl; // true
使用optional包装原始类型意味着需要存储原始类型的空间和额外的boolean flag,因此optional对象将占有更多的内存空间。此外,optional对象的内存排列须遵循与内部对象一致的内存对齐准则。
template
class optional
{
bool _initialized;
std::aligned_storage_t _storage;
public:
// operations
};
假如sizeof(double) = 8,sizeof(int) = 4,则:
std::optional
std::optional