首页 > 编程语言 >C++17新特性探索:拥抱std::optional,让代码更优雅、更安全

C++17新特性探索:拥抱std::optional,让代码更优雅、更安全

时间:2024-09-13 11:35:11浏览次数:9  
标签:std 17 42 value assert include optional

std::optional

  1. 背景
    在编程时,我们经常会遇到可能会返回/传递/使用一个确定类型对象的场景。也就是说,这个对象可能有一个确定类型的值也可能没有任何值。因此,我们需要一种方法来模拟类似指针的语义:指针可以通过 nullptr来表示没有值。解决方法是定义该对象的同时再定义一个附加的 bool类型的值作为标志来表示该对象是否有值。std::optional<>提供了一种类型安全的方式来实现这种对象。
  2. 占用内存大小
    可选对象所需的内存等于内含对象的大小加上一个 bool类型的大小。因此,可选对象一般比内含对象大一个字节(可能还要加上内存对齐的空间开销)。可选对象不需要分配堆内存,并且对齐方式和内含对象相同。
#include <iostream>
#include <optional>

// 定义一个没有默认构造函数的类
class MyClass {
public:
    explicit MyClass(int value) : data(value) {}
    ~MyClass() {}

    int getData() const {
        return data;
    }

private:
    int data;
};

// 输出 std::optional 是否包含值
void check_optional_value(std::optional<MyClass>& opt) {
    if (opt) {
        std::cout << "Value present: " << opt->getData() << std::endl;
    } else {
        std::cout << "No value present." << std::endl;
    }
}

int main() {
    // 创建一个没有值的 std::optional<MyClass>
    std::optional<MyClass> opt1;
    check_optional_value(opt1);

    // 创建一个有值的 std::optional<MyClass>
    std::optional<MyClass> opt2{MyClass(42)};
    check_optional_value(opt2);

    // 尝试通过 emplace 添加值
    opt1.emplace(24);
    check_optional_value(opt1);

    // 尝试通过 operator= 添加值
    opt1 = MyClass(56);
    check_optional_value(opt1);

    return 0;
}

输出:
Size of i: 4 bytes
Size of St8optionalIiE: 8 bytes
Size of 7MyClass: 4 bytes
Size of St8optionalI7MyClassE: 8 bytes

然而,可选对象并不是简单的等价于附加了bool标志的内含对象。例如,在没有值的情况下,将不会调用内含对象的构造函数(通过这种方式,没有默认构造函数的内含类型也可以处于有效的默认状态)。

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

4.应用
(1)std::optional<>模拟了一个可以为空的任意类型的实例。它可以被用作成员、参数、返回值等。
下面的示例程序展示了将 std::optional<>用作返回值的一些功能:

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

// 如果可能的话把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.has_value()) {
            std::cout << "convert '" << s << "' to int: " << oi.value() << "\n";
        }
        else {
            std::cout << "can't convert '" << s << "' to int\n";
        }
    }
}

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

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

class Name
{
private:
    std::string first;
    std::optional<std::string> middle;
    std::string last;
public:
    Name (std::string f, std::optional<std::string> m, std::string l)
          : first{std::move(f)}, middle{std::move(m)}, last{std::move(l)} {
    }
    friend std::ostream& operator << (std::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"};
    std::cout << n << '\n';

    Name m{"Donald", "Ervin", "Knuth"};
    std::cout << m << '\n';
}

5.std::optional<>类型和操作
(1)std::optional<>类型标准库在头文件 中以如下方式定义了 std::optional<>类:
namespace std {
template class optional;
}
另外还定义了下面这些类型和对象:
• std::nullopt_t类型的 std::nullopt,作为可选对象无值时候的“值”。
• 从 std::exception派生的 std::bad_optional_access异常类,当无值时候访问值将会抛出该异常。
可选对象还使用了 头文件中定义的 std::in_place对象(类型是 std::in_place_t)来支持用多个参数初始化可选对象(见下文)。
(2)std::optional<>的操作
表std::optional的操作列出了 std::optional<>的所有操作:

#include <iostream>
#include <optional>
#include <variant>
#include <vector>
#include <set>
#include <map>
#include <string>
#include <cmath>
#include <functional>
#include <cassert>
#include <complex>


// 使用命名空间简化代码
using namespace std::string_literals;

// 示例 1:构造 std::optional
void construct_optional() {
    std::optional<int> o1; // 不含有值
    assert(!o1.has_value());

    std::optional<int> o2(std::nullopt); // 显式表示不含有值
    assert(!o2.has_value());

    std::optional o3{42}; // 推导出 std::optional<int>
    assert(o3.has_value());
    assert(*o3 == 42);

    std::optional o4{"hello"}; // 推导出 std::optional<const char*>
    assert(o4.has_value());
    assert(*o4 == "hello");

    std::optional o5{"hello"s}; // 推导出 std::optional<std::string>
    assert(o5.has_value());
    assert(*o5 == "hello");

    // 用多个参数初始化可选对象
    std::optional<std::complex<double>> o6{std::in_place, 3.0, 4.0};
    assert(o6.has_value());
    assert(o6->real() == 3.0 && o6->imag() == 4.0);

    // 使用 std::make_optional
    auto o13 = std::make_optional(3.0); // std::optional<double>
    assert(o13.has_value());
    assert(*o13 == 3.0);

    auto o14 = std::make_optional("hello"); // std::optional<const char*>
    assert(o14.has_value());
    assert(*o14 == "hello");

    auto o15 = std::make_optional<std::complex<double>>(3.0, 4.0);
    assert(o15.has_value());
    assert(o15->real() == 3.0 && o15->imag() == 4.0);
}

// 示例 2:访问值
void access_optional_value() {
    std::optional<std::pair<int, std::string>> o{std::make_pair(42, "hello")};
    assert(o.has_value());
    assert(o->first == 42);
    assert(o->second == "hello");

    std::optional<std::string> o2{"hello"};
    assert(o2.has_value());
    assert(*o2 == "hello");

    // 当没有值时访问会导致未定义行为
    o2 = std::nullopt;
    assert(!o2.has_value());
    // std::cout << *o2 << std::endl; // 未定义行为
}

// 示例 3:使用 value_or
void use_value_or() {
    std::optional<std::string> o{"hello"};
    std::cout << o.value_or("NO VALUE") << std::endl; // 输出 "hello"

    o = std::nullopt;
    std::cout << o.value_or("NO VALUE") << std::endl; // 输出 "NO VALUE"
}

// 示例 4:比较
void compare_optionals() {
    std::optional<int> o0;
    std::optional<int> o1{42};
    assert(o0 == std::nullopt);
    assert(!(o0 == 42));
    assert(o0 < 42);
    assert(!(o0 > 42));
    assert(o1 == 42);
    assert(o0 < o1);
    assert(!(o0 > o1));

    std::optional<unsigned> uo;
    assert(uo < 0);
    assert(uo < -42);

    std::optional<bool> bo;
    assert(bo < false);

    std::optional<int> o2{42};
    std::optional<double> o3{42.0};
    assert(o2 == 42);
    assert(o3 == 42);
    assert(o2 == o3);
}

// 示例 5:修改值
void modify_optional_value() {
    std::optional<std::complex<double>> o; // 没有值
    std::optional<int> ox{77}; // optional<int>,值为77
    o = 42; // 值变为 complex(42.0, 0.0)
    assert(o.has_value());
    assert(o->real() == 42.0 && o->imag() == 0.0);

    o = std::complex<double>{9.9, 4.4}; // 值变为 complex(9.9, 4.4)
    assert(o.has_value());
    assert(o->real() == 9.9 && o->imag() == 4.4);

    o = ox; // OK,因为 int 转换为 complex<double>
    assert(o.has_value());
    assert(o->real() == 77.0 && o->imag() == 0.0);

    o = std::nullopt; // o 不再有值
    assert(!o.has_value());

    o.emplace(5.5, 7.7); // 值变为 complex(5.5, 7.7)
    assert(o.has_value());
    assert(o->real() == 5.5 && o->imag() == 7.7);

    o.reset(); // o 不再有值
    assert(!o.has_value());

    o = std::complex<double>{88.0, 0.0}; // OK:值变为 complex(88.0, 0.0)
    assert(o.has_value());
    assert(o->real() == 88.0 && o->imag() == 0.0);

    o = std::complex<double>{1.2, 3.4}; // OK:值变为 complex(1.2, 3.4)
    assert(o.has_value());
    assert(o->real() == 1.2 && o->imag() == 3.4);
}

// 示例 6:使用 lambda 初始化 set
void initialize_set_with_lambda() {
    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, 8, -7, -2, 0, 5},
                                                   sc};
    assert(o8.has_value());
    assert(o8->size() == 6);
}

int main() {
    construct_optional();
    access_optional_value();
    use_value_or();
    compare_optionals();
    modify_optional_value();
    initialize_set_with_lambda();
    return 0;
}

6.注意
(1)value()和 value_or()
value()和 value_or()之间有一个需要考虑的差异:4 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()是一个能够更清晰地表达意图的接口,但开销可能会更大一点。
(2)bool 类型或原生指针的可选对象
将可选对象用作 bool值时使用比较运算符会有特殊的语义。如果内含类型是 bool或者指针类型的话这可能导致令人迷惑的行为。例如:
std::optional ob{false}; // 值 为false
if (!ob) ... // 返 回false
if (ob == false) ... // 返 回true
std::optional<int*> op{nullptr};
if (!op) ... // 返 回false
if (op == nullptr) ... // 返 回true

标签:std,17,42,value,assert,include,optional
From: https://www.cnblogs.com/liudw-0215/p/18411917

相关文章

  • 【运维】aarch64安装JDK17
    1、下载安装包[root@jenkinsapp]#uname-aLinuxjenkins4.18.0-80.7.2.el7.aarch64#1SMPThuSep1216:13:20UTC2019aarch64aarch64aarch64GNU/Linux这里是aarch64架构,所以下载jdk需要下载对应......
  • 题解:CF1767E Algebra Flash
    可以在cnblogs中阅读。\(m\le40\)的数据范围提示让我们往颜色种类上考虑。由题每次可以跳\(1\)或\(2\)格,即存在一条从\(1\)到\(n\)的路径的充要条件是不存在两个相邻的未激活格。换句话说,对任意两个相邻的格子都必须选择至少一个激活。任意两个,至少一个,这样的条件......
  • 数据库tips17
    (十)、约束及索引类型约束的作用是为了防止可预见的错误的数据进入数据库中,是保障数据一致性的一种机制。UNIQUE约束是列级约束,表示关系中的记录在该列上的取值不重复。索引是通过建立索引列上的索引表,索引表中的査找项是索引列上的所有值的排序或散列(目的是为了快速查找),索引表中......
  • 2024.9.12 CF1783 VP
    A:先将\(a\)降序排序,此时只有位置\(2\)有可能不满足条件。找到最小的\(i\ge2\)使得\(a_1\neqa_i\)(不存在则无解),然后交换\(a_2,a_i\),即为一个解。点击查看代码#include<bits/stdc++.h>#defineintlonglong#definepsbkpush_back#definefstfirst#definescdse......
  • shujujiegou章节2.17
    /**Aclassthatmaintainsashoppingcartforanonlinestore.  @authorFrankM.Carrano  @version4.0*/publicclassOnlineShopper{  publicstaticvoidmain(String[]args)  {   Item[]items={newItem("Birdfeeder",2050),     ......
  • 1765asp.net古镇旅游网站VS开发sqlserver数据库web结构c#编程web网页设计
    博主介绍:专注于Java.net phpphython 小程序等诸多技术领域和毕业项目实战、企业信息化系统建设,从业十五余年开发设计教学工作☆☆☆精彩专栏推荐订阅☆☆☆☆☆不然下次找不到哟我的博客空间发布了1000+毕设题目方便大家学习使用感兴趣的可以先收藏起来,还有大家在......
  • 【F172】基于Springboot+vue实现的智能菜谱系统
    作者主页:Java码库主营内容:SpringBoot、Vue、SSM、HLMT、Jsp、PHP、Nodejs、Python、爬虫、数据可视化、小程序、安卓app等设计与开发。收藏点赞不迷路 关注作者有好处文末获取源码项目描述近些年,随着中国经济发展,人民的生活质量逐渐提高,对网络的依赖性越来越高,通过网......
  • 2024.09.12 1749版
    起于《海奥华预言》的思考◆地球管理结构和参考持续更新中...... 英文地址:https://github.com/zhuyongzhe/Earth/tags中文地址:https://www.cnblogs.com/zhuyongzhe85作者:朱永哲 ---------------------------------------------------------------------------------......
  • 掌握 C++17:结构化绑定与拷贝消除的妙用
    C++17特性示例1.结构化绑定(StructuredBinding)结构化绑定允许你用一个对象的元素或成员同时实例化多个实体。结构化绑定允许你在声明变量的同时解构一个复合类型的数据结构(如结构体,std::tuple,std::pair,或者std::array)。这样可以方便地获取多个值,而不需要显式地调用std::......
  • Python习题 177:设计银行账户类并实现存取款功能
    (编码题)Python实现一个简单的银行账户类BankAccount,包含初始化方法、存款、取款、获取余额等功能。参考答案分析需求如下。Python类BankAccount,用于模拟银行账户的基本功能。该类应包含以下方法:初始化方法:接受两个参数:account_holder(账户持有人的姓名)和balance(账户......