C++17完整导引-组件之std::optional

引子

  • 以一种表达的方式表示一个可能为空的对象
    在编程时,我们经常会遇到 可能 会返回、传递、使用一个确定类型对象的场景。也就是说,这个对象可能有一个确定类型的值也可能没有任何值。因此,我们需要一种方法来模拟类似指针的语义:指针可以通过nullptr来表示没有值。解决方法是定义该对象的同时再定义一个附加的bool类型的值作为标志来表示该对象是否有值std::optional<>提供了一种类型安全的方式来实现这种对象。
  • 开销
    可选对象所需的内存等于 内含 对象的大小加上一个bool类型的大小。因此,可选对象一般比内含对象大一个字节(可能还要加上内存对齐的空间开销)。**可选对象不需要分配堆内存,并且对齐方式和内含对象相同。**然而,可选对象并不是简单的等价于附加了bool标志的内含对象。例如,在没有值的情况下,将不会调用内含对象的构造函数(通过这种方式,没有默认构造函数的内含类型也可以处于有效的默认状态)。

While you can achieve “null-ability” by using unique values (-1, infinity, nullptr), it’s not as clear as the separate wrapper type. Alternatively, you could even use std::unique_ptr and treat the empty pointer as not initialized - this works, but comes with the cost of allocating memory for the object.
虽然您可以通过使用唯一值(-1, infinity, nullptr)来实现“空能力”,但它并不像单独的包装器类型那样清晰。或者,您甚至可以使用 std::unique_ptr 并将空指针视为未初始化 - 这有效,但会带来为对象分配内存的成本

  • 支持值语义和move语义
    std::variant<>std::any一样,可选对象有值语义。也就是说,拷贝操作会被实现为 深拷贝 :将创建一个新的独立对象,新对象在自己的内存空间内拥有原对象的标记和内含值(如果有的话)的拷贝。拷贝一个无内含值的std::optional<>的开销很小,但拷贝有内含值的std::optional<>的开销约等于拷贝内含值的开销。另外,std::optional<>对象也支持move语义

何时使用

通常,可以在以下情况下使用可选包装器:

  • 如果你想很好地表示一个可为空的类型。
    • 建议不要使用唯一值。(像 -1, nullptr, NO_VALUE 或者别的)
    • 例如,用户的中间名是可选的。您可以假设空字符串在这里有效,但了解用户是否输入了某些内容可能很重要。使用 std::optional<std::string>您可以获得更多信息。
  • 返回一些计算(处理)的结果,该结果无法生成值,并且不是错误。
    例如,在字典中查找元素:如果键下没有元素,则不是错误,但我们需要处理这种情况
  • 执行资源的延迟加载
    例如,资源类型没有默认构造函数,并且构造量很大。因此,您可以将其定义为 std::optional<Resource>(您可以在系统中传递它),然后仅在以后需要时才加载。
  • 将可选参数传递到函数中。
    使用boost::optional 类型的描述很好,它总结了我们何时应该使用这种类型, When to use Optional

    建议在以下情况下使用optional<T>:只有一个(对所有各方)明确的原因导致没有类型T的值,并且缺少值与具有任何常规T值一样自然

虽然有时使用optional的决定可能很模糊,但不应该将其用于错误处理。因为它最适合值为空并且是程序正常状态的情况。

使用std::optional<>

std::optional<>模拟了一个可以为空的任意类型的实例它可以被用作成员、参数、返回值等

可选的返回值

下面的示例程序展示了将std::optional<>用作返回值的一些功能:

#include <iostream>
#include <optional>
#include <string>

// 如果可能的话把string转换为int:
std::optional<int> asInt(const std::string& s) {
    try {
        return std::stoi(s);
    } catch (...) {
        return std::nullopt;
    }
}

int main() {
    for (auto s : {"42", "  077", "hello", "0x33"}) {
        // 尝试把s转换为int,并打印结果:
        std::optional<int> oi = asInt(s);
        if (oi) {
            std::cout << "convert '" << s << "' to int: " << *oi << "\n";
        } else {
            std::cout << "can't convert '" << s << "' to int\n";
        }
    }
}

运行结果:

convert '42' to int: 42
convert '  077' to int: 77
can't convert 'hello' to int
convert '0x33' to int: 0

预处理代码如下:

#include <iostream>
#include <optional>
#include <string>

// 如果可能的话把string转换为int:
std::optional<int> asInt(const std::basic_string<char, std::char_traits<char>, std::allocator<char> > & s)
{
  try 
  {
    return std::optional<int>(std::stoi(s, 0, 10));
  } catch(...) {
    return std::optional<int>(std::nullopt_t(std::nullopt));
  }
  ;
}

int main()
{
  {
    std::initializer_list<const char *> && __range1 = std::initializer_list<const char *>{"42", "  077", "hello", "0x33"};
    const char *const * __begin1 = __range1.begin();
    const char *const * __end1 = __range1.end();
    for(; __begin1 != __end1; ++__begin1) {
      const char * s = *__begin1;
      std::optional<int> oi = asInt(std::basic_string<char, std::char_traits<char>, std::allocator<char> >(s, std::allocator<char>()));
      if(static_cast<bool>(oi.operator bool())) {
        std::operator<<(std::operator<<(std::operator<<(std::operator<<(std::cout, "convert '"), s), "' to int: ").operator<<(oi.operator*()), "\n");
      } else {
        std::operator<<(std::operator<<(std::operator<<(std::cout, "can't convert '"), s), "' to int\n");
      } 
      
    }
    
  }
  return 0;
}

这段程序包含了一个asInt()函数来把传入的字符串转换为整数。然而这个操作有可能会失败,因此把返回值定义为std::optional<>,这样我们可以返回 “无整数值” 而不是约定一个特殊的int值,或者向调用者抛出异常来表示失败。
因此,当转换成功时我们用stoi()返回的int初始化返回值并返回,否则会返回std::nullopt来表明没有int值。

我们也可以像下面这样实现相同的行为:

std::optional<int> asInt(const std::string& s)
{
    std::optional<int> ret; // 初始化为无值
    try {
        ret = std::stoi(s);
    }
    catch (...) {
    }
    return ret;
}

main()中,我们用不同的字符串调用了这个函数:

for (auto s : {"42", "  077", "hello", "0x33"} ) {
    // 尝试把s转换为int,并打印结果:
    std::optional<int> oi = asInt(s);
    ...
}

对于返回的std::optional<int>类型的oi,我们可以判断它是否含有值(将该对象用作布尔表达式)并通过“解引用”的方式访问了该可选对象的值:

if (oi) {
    std::cout << "convert '" << s << "' to int: " << *oi << "\n";
}

注意用字符串"0x33"调用asInt()将会返回0,因为stoi()不会以十六进制的方式来解析字符串。还有一些别的方式来处理返回值,例如:

std::optional<int> oi = asInt(s);
if (oi.has_value()) {
    std::cout << "convert '" << s << "' to int: " << oi.value() << "\n";
}

这里使用了has_value()检查是否返回了一个值,还使用了value()来访问值。value()比运算符*更安全:当没有值时它会抛出一个异常。运算符*应该只用于已经确定含有值的场景,否则程序将可能有未定义的行为
完整示例如下:

#include <iostream>
#include <optional>
#include <string>

std::optional<int> asInt1(const std::string& s) {
    std::optional<int> ret;  // 初始化为无值
    try {
        ret = std::stoi(s);
    } catch (...) {
    }
    return ret;
}

int main() {
    for (auto s : {"42", "  077", "hello", "0x33"}) {
        // 尝试把s转换为int,并打印结果:
        std::optional<int> oi = asInt1(s);
        if (oi) {
            std::cout << "convert '" << s << "' to int: " << *oi << "\n";
        } else {
            std::cout << "can't convert '" << s << "' to int\n";
        }
    }
    std::cout << "--------------" << std::endl;
    for (auto s : {"42", "  077", "hello", "0x33"}) {
        std::optional<int> oi = asInt1(s);
        if (oi.has_value()) {
            std::cout << "convert '" << s << "' to int: " << oi.value() << "\n";
        } else {
            std::cout << "can't convert '" << s << "' to int\n";
        }
    }
}

运行结果:

convert '42' to int: 42
convert '  077' to int: 77
can't convert 'hello' to int
convert '0x33' to int: 0
--------------
convert '42' to int: 42
convert '  077' to int: 77
can't convert 'hello' to int
convert '0x33' to int: 0

**注意,**我们现在可以使用新的类型std::string_view和新的快捷函数std::from_chars()来改进asInt()

可选的参数和数据成员

另一个使用std::optional<>的例子是传递可选的参数设置可选的数据成员

#include <iostream>
#include <optional>
#include <string>

using namespace std;

class Name {
   private:
    string first;
    optional<std::string> middle;
    string last;

   public:
    Name(string f, optional<std::string> m, string l)
        : first{std::move(f)}, middle{std::move(m)}, last{std::move(l)} {}
    friend ostream& operator<<(ostream& strm, const Name& n) {
        strm << n.first << ' ';
        if (n.middle) {
            strm << *n.middle << ' ';
        }
        return strm << n.last;
    }
};

int main() {
    Name n{"Jim", std::nullopt, "Knopf"};
    cout << n << endl;

    Name m{"Donald", "Ervin", "Knuth"};
    cout << m << endl;
}

运行结果:

Jim Knopf
Donald Ervin Knuth

Name代表了一个由名、可选的中间名、姓组成的姓名。成员middle被定义为可选的,当没有中间名时可以传递一个std::nullopt这和中间名是空字符串是不同的状态
注意和通常值语义的类型一样,最佳的定义构造函数的方式是以值传参,然后把参数的值移动到成员里。

注意std::optional<>改变了成员middle的值的使用方式。直接使用n.middle将是一个布尔表达式,表示是否有中间名。使用*n.middle可以访问当前的值(如果有值的话)。
另一个访问值的方法是使用成员函数value_or()当没有值的时候可以指定一个备选值。例如,在类Name里我们可以实现为:

std::cout << middle.value_or("");   // 打印中间名或空

然而,这种方式下,当没有值时名和姓之间将有两个空格而不是一个

另外的示例

#include <iostream>
#include <optional>

class UserRecord {
   public:
    UserRecord(const std::string& name, std::optional<std::string> nick,
               std::optional<int> age)
        : mName{name}, mNick{nick}, mAge{age} {}

    friend std::ostream& operator<<(std::ostream& stream,
                                    const UserRecord& user);

   private:
    std::string mName;
    std::optional<std::string> mNick;
    std::optional<int> mAge;
};

std::ostream& operator<<(std::ostream& os, const UserRecord& user) {
    os << user.mName << ' ';
    if (user.mNick) {
        os << *user.mNick << ' ';
    }
    if (user.mAge) os << "age of " << *user.mAge;

    return os;
}

int main() {
    UserRecord tim{"Tim", "SuperTim", 16};
    UserRecord nano{"Nathan", std::nullopt, std::nullopt};

    std::cout << tim << "\n";
    std::cout << nano << "\n";
}

运行结果:

Tim SuperTim age of 16
Nathan 

std::optional<>类型和操作

std::optional<>类型

标准库在头文件<optional>中以如下方式定义了std::optional<>类:

namespace std {
    template<typename T> class optional;
}

另外还定义了下面这些类型和对象:

  • std::nullopt_t类型的std::nullopt,作为可选对象无值时候的“值”。
    std::nullopt_t 是空类类型,用于指示 optional 类型拥有未初始化状态。特别是 std::optional 有一个以 nullopt_t 为单参数的构造函数,它创建不含值的optional
  • std::exception派生的std::bad_optional_access异常类,当无值时候访问值将会抛出该异常
    可选对象还使用了<utility>头文件中定义的std::in_place对象(类型是std::in_place_t)来支持用多个参数初始化可选对象。

std::optional<>的操作

std::optional的操作列出了std::optional<>的所有操作:

操作符效果
构造函数创建一个可选对象(可能会调用内含类型的构造函数也可能不会)
make_optional<>()创建一个用参数初始化的可选对象
析构函数销毁一个可选对象
=赋予一个新值
emplace()给内含类型赋予一个新值
reset()销毁值(使对象变为无值状态)
has_value()返回可选对象是否含有值
转换为bool返回可选对象是否含有值
*访问内部的值(如果无值将会产生未定义行为)
->访问内部值的成员(如果无值将会产生未定义行为)
value()访问内部值(如果无值将会抛出异常)
value_or()访问内部值(如果无值将返回参数的值)
swap()交换两个对象的值
==、!=、<、<=、>、>=比较可选对象
hash<>计算哈希值的函数对象的类型

构造函数

特殊的构造函数允许你直接传递内含类型的值作为参数。

  • 你可以创建一个不含有值的可选对象。这种情况下,你必须指明内含的类型:
std::optional<int> o1;
std::optional<int> o2(std::nullopt);

这种情况下将不会调用内含类型的任何构造函数

  • 你可以传递一个值来初始化内含类型。得益于推导指引,你不需要再指明内含类型:
std::optional o3{42};       // 推导出optional<int>
std::optional o4{"hello"};  // 推导出optional<const char*>
using namespace std::string_literals;
std::optional o5{"hello"s}; // 推导出optional<string>
  • 为了用多个参数初始化可选对象,你必须传递一个构造好的对象或者添加std::in_place作为第一个参数(这种情况下推导不出内含类型):
std::optional o6{std::complex{3.0, 4.0}};
std::optional<std::complex<double>> o7{std::in_place, 3.0, 4.0};

注意第二种方式避免了创建临时变量。通过使用这种方式,你甚至可以传递一个初值列加上其他参数:

// 用lambda作为排序准则初始化set:
auto sc = [] (int x, int y) {
              return std::abs(x) < std::abs(y);
          };
std::optional<std::set<int, decltype(sc)>> o8{std::in_place, {4, 8, -7, -2, 0, 5}, sc};

然而,只有当所有的初始值都和容器里元素的类型匹配时才可以这么写。否则,你必须显式传递一个std::initializer_list<>

// 用lambda作为排序准则初始化set
auto sc = [] (int x, int y) {
              return std::abs(x) < std::abs(y);
          };
std::optional<std::set<int, decltype(sc)>> o8{std::in_place,
                                              std::initializer_list<int>{4, 5L}, sc};
  • 如果底层类型支持拷贝的话可选对象也可以拷贝(支持类型转换):
std::optional o9{"hello"};          // 推导出optional<const char*>
std::optional<std::string> o10{o9}; // OK

然而,注意如果内含类型本身可以用一个可选对象来构造,那么将会优先用可选对象构造内含对象,而不是拷贝:

std::optional<int> o11;
std::optional<std::any> o12{o11}; // o12内含了一个any对象,该对象的值是一个空的optional<int>

注意还有一个快捷函数make_optional<>()它可以用单个或多个参数初始化一个可选对象(不需要再使用in_place参数)。像通常的make...函数一样,它的参数也会退化:

auto o13 = std::make_optional(3.0);     // optional<double>
auto o14 = std::make_optional("hello"); // optional<const char*>
auto o15 = std::make_optional<std::complex<double>>(3.0, 4.0);

然而,注意没有一个构造函数可以根据参数的值来判断是应该用某个值还是用nullopt来初始化可选对象。这种情形下,只能使用运算符?:

例如:

std::multimap<std::string, std::string> englishToGerman;
...
auto pos = englishToGerman.find("wisdom");
auto o16 = pos != englishToGerman.end() ? std::optional{pos->second} : std::nullopt;

这个例子中,根据类模板参数推导,通过表达式std::optional{pos->second}能推导出o16的类型是std::optional<std::string>。类模板参数推导不能对单独的std::nullopt生效,但通过使用运算符?:std::nullopt也会自动转换成std::optional<string>类型,这是因为?:运算符的两个表达式必须有相同的类型

综合示例:

#include <complex>
#include <iostream>
#include <optional>
#include <string>
#include <vector>

int main() {
    std::optional<int> o1,  // 空
        o2 = 1,             // 从右值初始化
        o3 = o2;            // 复制构造函数

    // 调用 std::string( initializer_list<CharT> ) 构造函数
    std::optional<std::string> o4(std::in_place, {'a', 'b', 'c'});

    // 调用 std::string( size_type count, CharT ch ) 构造函数
    std::optional<std::string> o5(std::in_place, 3, 'A');

    // 从 std::string 移动构造,用推导指引拾取类型

    std::optional o6(std::string{"deduction"});

    std::cout << *o2 << ' ' << *o3 << ' ' << *o4 << ' ' << *o5 << ' ' << *o6
              << '\n';

    // empty:
    std::optional<int> oEmpty;
    std::optional<float> oFloat = std::nullopt;

    // direct:
    std::optional<int> oInt(10);
    std::optional oIntDeduced(10);  // 推到指引

    // make_optional
    auto oDouble = std::make_optional(3.0);
    auto oComplex = std::make_optional<std::complex<double>>(3.0, 4.0);

    // in_place
    std::optional<std::complex<double>> o7{std::in_place, 3.0, 4.0};

    // will call vector with direct init of {1, 2, 3}
    std::optional<std::vector<int>> oVec(std::in_place, {1, 2, 3});

    // copy/assign:
    auto oIntCopy = oInt;

    std::cout << oInt.value() << ' ' << oIntDeduced.value() << ' '
              << oDouble.value() << ' ' << oComplex.value() << ' ' << o7.value()
              << " " << oVec.value().size() << " " << oInt.value() << std::endl;
}

输出结果如下:

1 1 abc AAA deduction
10 10 3 (3,4) (3,4) 3 10

预处理代码如下:

#include <complex>
#include <iostream>
#include <optional>
#include <string>
#include <vector>

int main()
{
  std::optional<int> o1 = std::optional<int>();
  std::optional<int> o2 = std::optional<int>(1);
  std::optional<int> o3 = std::optional<int>(o2);
  std::optional<std::basic_string<char, std::char_traits<char>, std::allocator<char> > > o4 = std::optional<std::basic_string<char, std::char_traits<char>, std::allocator<char> > >(std::in_place_t(std::in_place), std::initializer_list<char>{'a', 'b', 'c'});
  std::optional<std::basic_string<char, std::char_traits<char>, std::allocator<char> > > o5 = std::optional<std::basic_string<char, std::char_traits<char>, std::allocator<char> > >(std::in_place_t(std::in_place), 3, 'A');
  std::optional<std::basic_string<char, std::char_traits<char>, std::allocator<char> > > o6 = std::optional<std::basic_string<char, std::char_traits<char>, std::allocator<char> > >(std::basic_string<char, std::char_traits<char>, std::allocator<char> >{"deduction", std::allocator<char>()});
  std::operator<<(std::operator<<(std::operator<<(std::operator<<(std::operator<<(std::operator<<(std::operator<<(std::operator<<(std::cout.operator<<(o2.operator*()), ' ').operator<<(o3.operator*()), ' '), o4.operator*()), ' '), o5.operator*()), ' '), o6.operator*()), '\n');
  std::optional<int> oEmpty = std::optional<int>();
  std::optional<float> oFloat = std::optional<float>(std::nullopt_t(std::nullopt));
  std::optional<int> oInt = std::optional<int>(10);
  std::optional<int> oIntDeduced = std::optional<int>(10);
  std::optional<double> oDouble = std::make_optional(3.0);
  std::optional<std::complex<double> > oComplex = std::make_optional<std::complex<double> >(3.0, 4.0);
  std::optional<std::complex<double> > o7 = std::optional<std::complex<double> >{std::in_place_t(std::in_place), 3.0, 4.0};
  std::optional<std::vector<int, std::allocator<int> > > oVec = std::optional<std::vector<int, std::allocator<int> > >(std::in_place_t(std::in_place), std::initializer_list<int>{1, 2, 3});
  std::optional<int> oIntCopy = std::optional<int>(oInt);
  std::operator<<(std::operator<<(std::operator<<(std::operator<<(std::operator<<(std::operator<<(std::operator<<(std::operator<<(std::cout.operator<<(oInt.value()), ' ').operator<<(oIntDeduced.value()), ' ').operator<<(oDouble.value()), ' '), oComplex.value()), ' '), o7.value()), " ").operator<<(oVec.value().size()), " ").operator<<(oInt.value()).operator<<(std::endl);
  return 0;
}

示例2

#include <iomanip>
#include <iostream>
#include <optional>
#include <string>
#include <vector>

int main() {
    auto op1 = std::make_optional<std::vector<char>>({'a', 'b', 'c'});
    std::cout << "op1: ";
    for (char c : op1.value()) {
        std::cout << c << ",";
    }
    auto op2 = std::make_optional<std::vector<int>>(5, 2);
    std::cout << "\nop2: ";
    for (int i : *op2) {
        std::cout << i << ",";
    }
    std::string str{"hello world"};
    auto op3 = std::make_optional<std::string>(std::move(str));
    std::cout << "\nop3: " << quoted(op3.value_or("empty value")) << '\n';
    std::cout << "str: " << std::quoted(str) << '\n';
}

运行输出如下:

op1: a,b,c,
op2: 2,2,2,2,2,
op3: "hello world"
str: ""

预处理代码如下:

int main()
{
  std::optional<std::vector<char, std::allocator<char> > > op1 = std::make_optional<std::vector<char, std::allocator<char> > >(std::initializer_list<char>{'a', 'b', 'c'});
  std::operator<<(std::cout, "op1: ");
  {
    std::vector<char, std::allocator<char> > & __range1 = op1.value();
    __gnu_cxx::__normal_iterator<char *, std::vector<char, std::allocator<char> > > __begin1 = __range1.begin();
    __gnu_cxx::__normal_iterator<char *, std::vector<char, std::allocator<char> > > __end1 = __range1.end();
    for(; !__gnu_cxx::operator==(__begin1, __end1); __begin1.operator++()) {
      char c = __begin1.operator*();
      std::operator<<(std::operator<<(std::cout, c), ",");
    }
    
  }
  std::optional<std::vector<int, std::allocator<int> > > op2 = std::make_optional<std::vector<int, std::allocator<int> > >(5, 2);
  std::operator<<(std::cout, "\nop2: ");
  {
    std::vector<int, std::allocator<int> > & __range1 = op2.operator*();
    __gnu_cxx::__normal_iterator<int *, std::vector<int, std::allocator<int> > > __begin1 = __range1.begin();
    __gnu_cxx::__normal_iterator<int *, std::vector<int, std::allocator<int> > > __end1 = __range1.end();
    for(; !__gnu_cxx::operator==(__begin1, __end1); __begin1.operator++()) {
      int i = __begin1.operator*();
      std::operator<<(std::cout.operator<<(i), ",");
    }
    
  }
  std::basic_string<char, std::char_traits<char>, std::allocator<char> > str = std::basic_string<char, std::char_traits<char>, std::allocator<char> >{"hello world", std::allocator<char>()};
  std::optional<std::basic_string<char, std::char_traits<char>, std::allocator<char> > > op3 = std::make_optional<std::basic_string<char, std::char_traits<char>, std::allocator<char> > >(std::move(str));
  std::operator<<(std::__detail::operator<<(std::operator<<(std::cout, "\nop3: "), std::quoted(op3.value_or<const char (&)[12]>("empty value"), char('"'), char('\\'))), '\n');
  std::operator<<(std::__detail::operator<<(std::operator<<(std::cout, "str: "), std::quoted(str, char('"'), char('\\'))), '\n');
  return 0;
}

访问值

几种选择:

  • operator* operator->- 返回指向所含值的指针或到它的引用。 解引用运算符operator*()不检查此optional是否含值,它可能比 value() 更有效率。若 *this 不含值则行为未定义
  • value() - 若*this含值,则返回到所含值引用。否则,抛出 std::bad_optional_access 异常。
  • value_or(defaultVal) - 返回值(如果可用),否则返回默认值。

为了检查一个可选对象是否有值,你可以调用has_value()或者在bool表达式中使用它:

std::optional o{42};

if (o) ...              // true
if (!o) ...             // false
if (o.has_value())...   // true

预处理代码如下:

  std::optional<int> o = std::optional<int>{42};
  if(static_cast<bool>(o.operator bool())) {
  } 
  
  if(!static_cast<bool>(o.operator bool())) {
  } 
  
  if(o.has_value()) {
  } 

没有为可选对象定义I/O运算符,因为当可选对象无值时不确定应该输出什么

std::cout << o;         // ERROR

要访问内部值可以使用指针语法。也就是说,通过运算符*,你可以直接访问可选对象的内部值,也可以使用->访问内部值的成员:

std::optional o{std::pair{42, "hello"}};
auto p = *o;            // 初始化p为pair<int, string>
std::cout << o->first;  // 打印出42

注意这些操作符都需要可选对象内含有值。在没有值的情况下这样使用会导致未定义行为:

std::optional<std::string> o{"hello"};

std::cout << *o;    // OK:打印出"hello"
o = std::nullopt;
std::cout << *o;    // 未定义行为

注意在实践中第二个输出语句仍能正常编译并可能再次打印出"hello",因为可选对象里底层值的内存并没有被修改。然而,你绝不应该依赖这一点。如果你不知道一个可选对象是否有值,你必须像下面这样调用

if (o) std::cout << *o;     // OK(可能输出为空字符串)

或者你可以使用value()成员函数来访问值,当内部没有值时将抛出一个std::bad_optional_access异常:

std::cout << o.value();     // OK(无值时会抛出异常)

std::bad_optional_access直接派生自std::exception

请注意operator*value()都是返回内含对象的引用。因此,当直接使用这些操作返回的临时对象时要小心。例如,对于一个返回可选字符串的函数:

std::optional<std::string> getString();

把它返回的可选对象的值赋给新对象总是安全的:

auto a = getString().value();           // OK:内含对象的拷贝或抛出异常

然而,直接使用返回值(或者作为参数传递)是麻烦的根源:

auto b = *getString();                  // ERROR:如果返回std::nullopt将会有未定义行为
const auto& r1 = getString().value();   // ERROR:引用销毁的内含对象
auto&& r2 = getString().value();        // ERROR:引用销毁的内含对象

使用引用的问题是:根据规则,引用会延长value()的返回值的生命周期, 而不是 getString()返回的可选对象的生命周期。因此,r1r2会引用不存在的值,使用它们将会导致未定义行为。注意当使用范围for循环时很容易出现这个问题:

std::optional<std::vector<int>> getVector();
...
for (int i : getVector().value()) {     // ERROR:迭代一个销毁的vector
    std::cout << i << '\n';
}

注意迭代一个non-optional的vector<int>类型的返回值是可以的。因此,不要盲目的把函数返回值替换为相应的可选对象类型。

最后,你可以在获取值时针对无值的情况设置一个fallback值。这通常是把一个可选对象写入到输出流的最简单的方式:

std::cout << o.value_or("NO VALUE");    // OK(没有值时写入NO VALUE)

然而,value()value_or()之间有一个需要考虑的差异:

value_or()返回值,而value()返回引用。这意味着如下调用:

std::cout << middle.value_or("");

和:

std::cout << o.value_or("fallback");

都会暗中分配内存,而value()永远不会。

然而,当在临时对象(rvalue)上调用value_or()时,将会 移动 走内含对象的值并以值返回,而不是调用拷贝函数构造。这是唯一一种能让value_or()适用于move-only的类型的方法,因为在左值(lvalue)上调用的value_or()的重载版本需要内含对象可以拷贝。

因此,上面例子中效率最高的实现方式是:

std::cout << o ? o->c_str() : "fallback";

而不是:

std::cout << o.value_or("fallback");

value_or()一个能够更清晰地表达意图的接口,但开销可能会更大一点

示例

#include <iomanip>
#include <iostream>
#include <optional>

int main() {
    // by operator*
    std::optional<int> oint = 10;
    std::cout << "oint " << *oint << '\n';

    // by value()
    std::optional<std::string> ostr("hello");
    try {
        std::cout << "ostr " << ostr.value() << '\n';
    } catch (const std::bad_optional_access& e) {
        std::cout << e.what() << "\n";
    }

    // by value_or()
    std::optional<double> odouble;  // empty
    std::cout << "odouble " << odouble.value_or(10.0) << '\n';
}

运行结果:

oint 10
ostr hello
odouble 10

预处理代码如下:

#include <iomanip>
#include <iostream>
#include <optional>

int main()
{
  std::optional<int> oint = std::optional<int>(10);
  std::operator<<(std::operator<<(std::cout, "oint ").operator<<(oint.operator*()), '\n');
  std::optional<std::basic_string<char, std::char_traits<char>, std::allocator<char> > > ostr = std::optional<std::basic_string<char, std::char_traits<char>, std::allocator<char> > >("hello");
  try 
  {
    std::operator<<(std::operator<<(std::operator<<(std::cout, "ostr "), ostr.value()), '\n');
  } catch(const std::bad_optional_access & e) {
    std::operator<<(std::operator<<(std::cout, e.what()), "\n");
  }
  ;
  std::optional<double> odouble = std::optional<double>();
  std::operator<<(std::operator<<(std::cout, "odouble ").operator<<(odouble.value_or<double>(10.0)), '\n');
  return 0;
}

比较

你可以使用通常的比较运算符。操作数可以是可选对象、内含类型的对象、std::nullopt

  • 如果两个操作数都是有值的对象,将会调用内含类型的相应操作符。
  • 如果两个操作数都是没有值的对象,那么它们相等(==、<=、>=返回true,其他比较返回false)。
  • 如果恰有一个操作数有值,那么无值的操作数小于有值的操作数。

例如:

std::optional<int> o0;
std::optional<int> o1{42};

    o0 == std::nullopt  // 返回true
    o0 == 42            // 返回false
    o0 < 42             // 返回true
    o0 > 42             // 返回false
    o1 == 42            // 返回true
    o0 < o1             // 返回true

这意味着unsigned int类型的可选对象,甚至可能小于0:

std::optional<unsigned> uo;
    uo < 0              // 返回true
    uo < -42            // 返回true

对于bool类型的可选对象,也可能小于false

std::optional<bool> bo;
    bo < false          // 返回true

为了让代码可读性更强,应该使用

if (!uo.has_value())

而不是

if (uo < 0)

可选对象和底层类型之间的混合比较也是支持的,前提是底层类型支持这种比较:

std::optional<int> o1{42};
std::optional<double> o2{42.0};

o2 == 42            // 返回true
o1 == o2            // 返回true

如果底层类型支持隐式类型转换,那么相应的可选对象类型也会进行隐式类型转换。
注意可选的bool类型或原生指针可能会导致一些奇怪的行为。

示例:

#include <optional>
#include <iostream>

int main()
{
    std::optional<int> oEmpty;
    std::optional<int> oTwo(2);
    std::optional<int> oTen(10);

    std::cout << std::boolalpha;
    std::cout << (oTen > oTwo) << "\n";
    std::cout << (oTen < oTwo) << "\n";
    std::cout << (oEmpty < oTwo) << "\n";
    std::cout << (oEmpty == std::nullopt) << "\n";
    std::cout << (oTen == 10) << "\n";
}

运行结果如下:

true
false
true
true
true

预处理代码如下:

  std::optional<int> oEmpty = std::optional<int>();
  std::optional<int> oTwo = std::optional<int>(2);
  std::optional<int> oTen = std::optional<int>(10);
  std::cout.operator<<(std::boolalpha);
  std::operator<<(std::cout.operator<<((std::operator>(oTen, oTwo))), "\n");
  std::operator<<(std::cout.operator<<((std::operator<(oTen, oTwo))), "\n");
  std::operator<<(std::cout.operator<<((std::operator<(oEmpty, oTwo))), "\n");
  std::operator<<(std::cout.operator<<((std::operator==(oEmpty, std::nullopt_t(std::nullopt)))), "\n");
  std::operator<<(std::cout.operator<<((std::operator==(oTen, 10))), "\n");

修改值

赋值运算和emplace()操作可以用来修改值:

std::optional<std::complex<double>> o;  // 没有值
std::optional ox{77};   // optional<int>,值为77

o = 42;                 // 值变为complex(42.0, 0.0)
o = {9.9, 4.4};         // 值变为complex(9.9, 4.4)
o = ox;                 // OK,因为int转换为complex<double>
o = std::nullopt;       // o不再有值
o.emplace(5.5, 7.7);    // 值变为complex(5.5, 7.7)

赋值为std::nullopt会移除内含值,如果之前有值的话将会调用内含类型的析构函数。你也可以通过调用reset()实现相同的效果:

o.reset();              // o不再有值

或者赋值为空的花括号:

o = {};                 // o不再有值

最后,我们也可以使用operator*来修改值,因为它返回的是引用。然而,注意这种方式要求值必须存在:

std::optional<std::complex<double>> o;
*o = 42;                // 未定义行为
...
if (o) {
    *o = 88;            // OK:值变为complex(88.0, 0.0)
    *o = {1.2, 3.4};    // OK:值变为complex(1.2, 3.4)
}

示例:

#include <iostream>
#include <optional>
#include <string>

class UserName {
   public:
    explicit UserName(const std::string& str) : mName(str) {
        std::cout << "UserName::UserName(\'";
        std::cout << mName << "\')\n";
    }
    ~UserName() {
        std::cout << "UserName::~UserName(\'";
        std::cout << mName << "\')\n";
    }

   private:
    std::string mName;
};

int main() {
    std::optional<UserName> oEmpty;

    // emplace:
    oEmpty.emplace("Steve");

    // calls ~Steve and creates new Mark:
    oEmpty.emplace("Mark");

    // reset so it's empty again
    oEmpty.reset();  // calls ~Mark
    // same as:
    // oEmpty = std::nullopt;

    // assign a new value:
    oEmpty.emplace("Fred");
    oEmpty = UserName("Joe");
}

运行结果:

UserName::UserName('Steve')
UserName::~UserName('Steve')
UserName::UserName('Mark')
UserName::~UserName('Mark')
UserName::UserName('Fred')
UserName::UserName('Joe')
UserName::~UserName('Joe')
UserName::~UserName('Joe')

预处理代码如下:

#include <iostream>
#include <optional>
#include <string>

class UserName
{
  public: 
  inline explicit UserName(const std::basic_string<char, std::char_traits<char>, std::allocator<char> > & str)
  : mName{std::basic_string<char, std::char_traits<char>, std::allocator<char> >(str)}
  {
    std::operator<<(std::cout, "UserName::UserName('");
    std::operator<<(std::operator<<(std::cout, this->mName), "')\n");
  }
  inline ~UserName() noexcept
  {
    std::operator<<(std::cout, "UserName::~UserName('");
    std::operator<<(std::operator<<(std::cout, this->mName), "')\n");
  }
  private: 
  std::basic_string<char, std::char_traits<char>, std::allocator<char> > mName;
  public: 
  // inline constexpr UserName(const UserName &) noexcept(false) = default;
  // inline UserName & operator=(const UserName &) noexcept(false) = default;
};

int main()
{
  std::optional<UserName> oEmpty = std::optional<UserName>();
  oEmpty.emplace<const char (&)[6]>("Steve");
  oEmpty.emplace<const char (&)[5]>("Mark");
  oEmpty.reset();
  oEmpty.emplace<const char (&)[5]>("Fred");
  oEmpty.operator=(UserName(UserName(std::basic_string<char, std::char_traits<char>, std::allocator<char> >("Joe", std::allocator<char>()))));
  return 0;
}

move语义

std::optional<>也支持move语义。如果你move了整个可选对象,那么内部的状态会被拷贝,值会被move。因此,被move可选对象仍保持原来的状态,但值变为未定义。然而,你也可以单独把内含的值移进或移出。例如:

std::optional<std::string> os;
std::string s = "a very very very long string";
os = std::move(s);                  // OK,move
std::string s2 = *os;               // OK,拷贝
std::string s3 = std::move(*os);    // OK,move

预处理代码如下:

{
  std::optional<std::basic_string<char, std::char_traits<char>, std::allocator<char> > > os = std::optional<std::basic_string<char, std::char_traits<char>, std::allocator<char> > >();
  std::basic_string<char, std::char_traits<char>, std::allocator<char> > s = std::basic_string<char, std::char_traits<char>, std::allocator<char> >("a very very very long string", std::allocator<char>());
  os.operator=(std::move(s));
  std::basic_string<char, std::char_traits<char>, std::allocator<char> > s2 = std::basic_string<char, std::char_traits<char>, std::allocator<char> >(os.operator*());
  std::basic_string<char, std::char_traits<char>, std::allocator<char> > s3 = std::basic_string<char, std::char_traits<char>, std::allocator<char> >(std::move(os.operator*()));
  return 0;
}

注意在最后一次调用之后,os仍然含有一个字符串值,但就像值被移走的对象一样,这个值是未定义的。因此,你可以使用它,但不要对它的值有任何假设。你也可以给它赋一个新的字符串。另外注意有些重载版本会保证临时的可选对象被move。考虑下面这个返回一个可选字符串的函数:

std::optional<std::string> func();

在这种情况下,下面的代码将会move临时可选对象的值:

std::string s4 = func().value();    // OK,move
std::string s5 = *func();           // OK,move

可以通过重载相应成员函数的右值版本来保证上述的行为:

namespace std {
    template<typename T>
    class optional {
        ...
        constexpr T& operator*() &;
        constexpr const T& operator*() const&;
        constexpr T&& operaotr*() &&;
        constexpr const T&& operator*() const&&;
        constexpr T& value() &;
        constexpr const T& value() const&;
        constexpr T&& value() &&;
        constexpr const T&& value() const&&;
    };
}

换句话说,你也可以像下面这样写:

std::optional<std::string> os;
std::string s6 = std::move(os).value(); // OK,move

哈希

可选对象的哈希值就等于内含值的哈希值(如果有值的话)。无值的可选对象的哈希值未定义。

特殊情况

一些特定的可选类型可能会导致特殊或意料之外的行为。

bool类型或原生指针的可选对象

可选对象用作bool值时使用比较运算符会有特殊的语义。如果内含类型是bool或者指针类型的话这可能导致令人迷惑的行为。例如:

std::optional<bool> ob{false};  // 值为false
if (!ob) ...                    // 返回false
if (ob == false) ...            // 返回true

std::optional<int*> op{nullptr};
if (!op) ...                    // 返回false
if (op == nullptr) ...          // 返回true

可选对象的可选对象

理论上讲,你可以定义可选对象的可选对象:

std::optional<std::optional<std::string>> oos1;
std::optional<std::optional<std::string>> oos2 = "hello";
std::optional<std::optional<std::string>> oos3{std::in_place, std::in_place, "hello"};

std::optional<std::optional<std::complex<double>>> ooc{std::in_place, std::in_place, 4.2, 5.3};

你甚至可以通过隐式类型转换直接赋值:

oos1 = "hello";         // OK:赋新值
ooc.emplace(std::in_place, 7.2, 8.3);

因为两层可选对象都可能没有值,可选对象的可选对象允许你在内层无值或者在外层无值,这可能会导致不同的语义:

*oos1 = std::nullopt;   // 内层可选对象无值
oos1 = std::nullopt;    // 外层可选对象无值

这意味着在处理这种可选对象的时候你必须特别小心:

if (!oos1) std::cout << "no value\n";
if (oos1 && !*oos1) std::cout << "no inner value\n";
if (oos1 && *oos1) std::cout << "value: " << **oos1 << '\n';

然而,从语义上来看,这只是一个有两种状态都代表无值的类型而已。因此,带有两个bool值或monostatestd::variant<>将是一个更好的替代。

  • 0
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

-西门吹雪

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值