Boost经验在C++17中的体现 :熟悉的特性在新标准中的应用

本文介绍了C++17如何受益于Boost库,特别是std::optional,std::variant,std::any和string_view的使用,展示了这些类型在代码表达力和性能优化方面的优势。
摘要由CSDN通过智能技术生成

一、简介

本文展示一些久经考验的特性,这些特性来自于著名的Boost库,它们被引入到了C++ 17中。Boost中的许多元素现在都是标准库的一部分,随着标准库中元素数量的增加,在Boost经验的支持下,可以编写更流畅的C++代码。

Boost库提供了大量标准库中没有的方便算法、类型和特性。许多功能被“移植”到C++的核心中。例如,在C++ 11中,获得了std::regex、线程和智能指针等。

因此,可以将Boost视为标准库之前的测试战场。C++的新标准中有很多元素是从Boost中迁移过来的。例如:

  • 词汇类型:std::variantstd::anystd::optional
  • string_view
  • 搜索算法:Boyer MooreBoyer Moore Horspool
  • std::filesystem
  • 特殊数学函数。
  • template 的改进。

这带来的好处是,如果只使用Boost的一小部分,如Boost::variantBoost::optional,现在可以使用几乎相同的代码并转换为标准库类型(std::variantstd::optional)。

让我们一起来了解C++中一些很酷的东西吧!

二、词汇类型

第一个是“词汇类型(Vocabulary Types)”。能够编写富有表现力的代码是一项引人注目的能力。有时,仅使用内置类型并不能提供这些选项。例如,可以设置一些数字并将其赋值为“NOT_NUMBER”,或者将-1的值视为空项。作为“终极解决方案”,甚至可以使用指针并将nullptr视为null,但是从标准中获得显式类型不是更好吗?

如果使用过Boost,那么可能会遇到Boost::optionalBoost::variantBoost::any这样的类型。

不是将-1视为“空数”,而是利用optional<int> :如果optional是“空”,那么没有一个数字,这就很简单。

另外,variant<string, int, float>是允许存储三种可能的类型并在运行时在它们之间切换的类型。

最后,还有一些类似于动态语言中的var类型;它可以存储任何类型并动态更改它们。它可能是整型,之后可以把它转换成字符串。

2.1、std::optional

第一个是std::optional,先看一下代码:

template <typename Map, typename Key>
std::optional<typename Map::value_type::second_type> TryFind(const Map& m, const Key& k) {
    auto it = m.find(k);
    if (it != m.end())
        return std::make_optional(it->second);
    return std::nullopt;
}

TryFind返回存储在映射中的可选值或空值。

TryFind使用方式:

std::map<std::string, int> mm { {"hello", 10}, { "super", 42 }};
auto ov = TryFind(mm, "hello");

// one:
std::cout << ov.value_or(0) << '\n';

// two:
if (ov)
    std::cout << *ov << '\n';

如果可选的ov包含一个值,可以通过.value()成员函数或操作符*访问它。在上面的代码中,使用了另一种替代方法,即value_or()函数,它返回存在的值或返回传递的参数。

示例代码:

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

template <typename Map, typename Key>
std::optional<typename Map::value_type::second_type> TryFind(const Map& m, const Key& k) {
    auto it = m.find(k);
    if (it != m.end())
        return std::make_optional(it->second);

    return std::nullopt;
}

int main() {
    std::map<std::string, int> mm { {"hello", 10}, { "super", 12 }};
    auto ov = TryFind(mm, "hello");
    std::cout << ov.value_or(0) << '\n';
        
    return 0;
}

2.2、std::variant

std::optional存储一个值或者什么都不存储,那么在安全联合类型中存储更多类型如何?示例代码:

std::variant<int, float, std::string> TryParseString(std::string_view sv) {
    // try with float first
    float fResult = 0.0f;
    const auto last = sv.data() + sv.size();
    const auto res = std::from_chars(sv.data(), last, fResult);
    if (res.ec != std::errc{} || res.ptr != last) {
        // if not possible, then just assume it's a string
        return std::string{sv};
    }
    // no fraction part? then just cast to integer
    if (static_cast<int>(fResult) == fResult)
        return static_cast<int>(fResult);
    return fResult;
}

std::variant可用于将不同类型存储为解析结果。一个常见的案例是解析命令行或某些配置文件。函数TryParseString接受一个字符串视图,然后尝试将其解析为floatintstring。如果浮点值没有分数部分,则将其存储为整数。否则,它就是float。如果不能执行数值转换,则该函数复制该字符串。

要访问存储在变体中的值,首先必须知道活动类型。下面的代码展示了如何做到这一点,并使用TryParseString的返回值:

const auto var = TryParseString("12345.98");
try {
    if (std::holds_alternative<int>(var))
        std::cout << "parsed as int: " << std::get<int>(var) << '\n';
    else if (std::holds_alternative<float>(var))
        std::cout << "parsed as float: " << std::get<float>(var) << '\n';
    else if (std::holds_alternative<string>(var))
        std::cout << "parsed as string: " << std::get<std::string>(var) << '\n';
}
catch (std::bad_variant_access&) {
    std::cout << "bad variant access...\n";
}

主要思想是使用std::holds_alternative(),它允许检查存在的类型。variant还提供了.index()成员函数,该函数返回从0…到存储类型的最大num的数字。

但是,最酷的用法之一是std::visit()。使用这个新功能,可以传递一个变体并访问主动存储的类型。要做到这一点,需要提供一个对给定变体中所有可能类型具有调用操作符的函子:

struct PrintInfo {
    void operator()(const int& i) const    { cout << "parsed as int" << i << '\n'; }
    void operator()(const float& f) const  { cout << "parsed as float" << f << '\n'; }
    void operator()(const string& s) const { cout << "parsed as str" << s << '\n'; }
};

auto PrintVisitorAuto = [](const auto& t) { std::cout << t << '\n'; };
const auto var = TryParseString("Hello World");
std::visit(PrintVisitorAuto , var);
std::visit(PrintInfo{}, var);

在上面的例子中,使用了两种类型的访问者。第一个是PrintInfo,它是一个结构体,提供了调用操作符的所有覆盖;可以使用它来显示关于给定类型的更多信息,并执行唯一的实现。另一个是PrintVisitorAuto,利用泛型lambda,如果所有类型的实现都是相同的,这是很方便的。

2.3、std::any

std::any可能是最不为人所知的词汇表类型,而且这种灵活类型的用例并不多。它很像JavaScript中的var,因为它可以保存任何东西。

使用示例:

struct property {
    property();
    property(const std::string &, const std::any &);
    std::string name;
    std::any value;
};
typedef std::vector<property> properties;

有了这样的属性类,可以存储任何类型。但是,如果可以知道可能类型的数量,那么最好使用std::variant,因为它比std::any执行得快(不需要额外的动态内存分配)。

更多用例可以参考Any Library Proposal for TR2 - https://www.open-std.org/jtc1/sc22/wg21/docs/papers/2006/n1939.html

2.4、其他

想了解更多的词汇类型以及它们的特性、用法、案例,可以阅读我的历史博客中写的单独的文章。

三、非所属字符串 std::string_view

std::string_view是连续字符序列的非归属视图。它在Boost中已经准备好几年了(参见Boost utils string_view)。现在boost版本与C++ 17版本string_view的接口是一致的。

从概念上讲,string_view由指向字符序列的指针和大小组成:

struct BasicCharStringView {
    char* dataptr;
    size_t size;
};

std::string_view有什么独特之处?

首先,string_viewchar*参数的自然替代品。如果函数接受const char*,然后对它执行一些操作,那么也可以使用视图,并且它的接口类似字符串的接口。

示例:

#include <iostream>
#include <string_view>

size_t CStyle(const char* str, char ch) {
    auto chptr = strchr(str, ch);
    if (chptr != nullptr)
        return strlen(str) + (chptr - str);
    
    return strlen(str);
}

size_t CppStyle(std::string_view sv, char ch) {
    auto pos = sv.find(ch);
    if (pos != std::string_view::npos)
        return sv.length() + pos;
    
    return sv.length();
}

int main() {
    std::cout << CStyle("Hello World", 'X') << '\n';
    std::cout << CppStyle("Hello World", 'X') << '\n';
        
    return 0;
}

类似地,有许多类似字符串的类实现。例如CString, QString等等,如果代码需要处理很多类型,string_view可能会有所帮助。那些其他类型可以提供对数据指针和大小的访问,然后可以创建一个string_view对象。

视图在处理大型字符串和切割较小的部分时也可能很有帮助。例如,在文件解析中,可以将文件内容加载到单个std::string对象中,然后使用视图执行处理。这可能会显示出很好的性能提升,因为不需要任何额外的字符串副本。

不过需要记住,因为string_view不拥有数据,也可能不是null终止,使用它有一些风险:

  • 小心(非)以NULL结尾的字符串:string_view可能在字符串的末尾不包含NULL。所以必须为这种情况做好准备。因为在调用像atoi, printf这样接受以null结束的字符串的函数时会出现问题。
  • 引用和临时对象问题:string_view不拥有内存,所以在处理临时对象时必须非常小心。例如从函数返回string_view时,或者将string_view存储在对象或容器中;都可能会出现问题。

另一个好消息:Boost中的starts_with()/ends_with()算法现在是C++ 20的一部分,许多编译器已经实现了它们。它们可用于string_viewstd::string

四、总结

希望这篇博文能给你更多开始使用C++ 17的动力。最新的C++17标准不仅提供了许多语言特性(如if constexpr、结构化绑定、折叠表达式、……),而且还提供了来自标准库的一组广泛的实用程序。现在可以使用多种词汇表类型:variantoptionalany。使用字符串视图,甚至使用一个重要的组件std::filesystem。所有这些都不需要引用一些外部库。

互动一下吧,在评论中分享你的经验。

  • 你最喜欢的Boost功能是什么?也许它们也会被合并到标准中?
  • 你是否将一些boost代码移植到C++ 17中?

在这里插入图片描述

  • 29
    点赞
  • 40
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 2
    评论
评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

打赏作者

Lion Long

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

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

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

打赏作者

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

抵扣说明:

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

余额充值