4-C++17-中那些值得关注的特性(下).md 20.0 KB
Newer Older
M
init  
miykael 已提交
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 281 282 283 284 285 286 287 288 289 290 291 292 293 294 295 296 297 298 299 300 301 302 303 304 305 306 307 308 309 310 311 312 313 314 315 316 317 318 319 320 321 322 323 324 325 326 327 328 329 330 331 332 333 334 335 336 337 338 339 340 341 342 343 344 345 346 347 348 349 350 351 352 353 354 355 356 357 358 359 360 361 362 363 364 365 366 367 368 369 370 371 372 373 374 375 376 377 378 379 380 381 382 383 384 385 386 387 388 389 390 391 392 393 394 395 396 397 398 399 400 401 402 403 404 405 406 407 408 409 410 411 412 413 414 415 416 417 418 419 420 421 422 423 424 425 426 427 428 429 430 431 432 433 434 435 436 437 438 439 440 441 442 443 444 445 446 447 448 449 450 451 452 453 454 455 456 457 458 459 460 461 462 463 464 465 466 467 468 469 470 471 472 473 474
## C++17 中那些值得关注的特性	(下)

文/祁宇

>接着前两篇文章,本文将介绍剩下的一些 C++ 17 特性,这些特性一部分来自于 boost 库,一部分则是用来让代码写得更加简洁便利,算是语法糖;还有一部分是新增算法。来自于 boost 库的特性有:variant、any、optional、filesystem 和 string _ view(string _ view 已在前文介绍过);让代码更加简洁便利的特性有:nested namespaces、single param staticassert、if init、deduction guide、capture this 等;还有一些是新增加的实用算法和并行算法。

### 来自 boost 库的特性
####  std::any

std::any 可以用来存放任意类型的对象,在某些时候可以用来做类型擦除。std::any 的用法比较简单,和 boost.any 的用法几乎是一致的,下面是它的基本用法。

```
void test_any()
    {
        std::any v1;
        int a = 2;
        v1 = a;
        if(v1.has_value())
            std::cout<<std::any_cast<int>(v1)<<'\n';
     
        double b = 3.0;
        v1 = b;
     
        if(v1.has_value())
            std::cout<<std::any_cast<double>(v1)<<'\n';
     
        try
        {
            std::cout<<std::any_cast<int>(v1)<<'\n';
        }
        catch(std::bad_any_cast& e)
        {
            std::cout<<e.what()<<'\n'; }="" v1.reset();="" if(!v1.has_value())="" std::cout<<"no="" value"<<'\n';="" }<="" pre="">
    <script type="text/javascript">
        function path()
        {
          var args = arguments,
              result = []
              ;
                
          for(var i = 0; i < args.length; i++)
              result.push(args[i].replace('@', '/cms/js/syntax/scripts/'));
                
          return result
        };
          
        SyntaxHighlighter.autoloader.apply(null, path(
          'applescript            @shBrushAppleScript.js',
          'actionscript3 as3      @shBrushAS3.js',
          'bash shell             @shBrushBash.js',
          'coldfusion cf          @shBrushColdFusion.js',
          'cpp c                  @shBrushCpp.js',
          'c# c-sharp csharp      @shBrushCSharp.js',
          'css                    @shBrushCss.js',
          'delphi pascal          @shBrushDelphi.js',
          'diff patch pas         @shBrushDiff.js',
          'erl erlang             @shBrushErlang.js',
          'groovy                 @shBrushGroovy.js',
          'java                   @shBrushJava.js',
          'jfx javafx             @shBrushJavaFX.js',
          'js jscript javascript  @shBrushJScript.js',
          'perl pl                @shBrushPerl.js',
          'php                    @shBrushPhp.js',
          'text plain             @shBrushPlain.js',
          'py python              @shBrushPython.js',
          'ruby rails ror rb      @shBrushRuby.js',
          'sass scss              @shBrushSass.js',
          'scala                  @shBrushScala.js',
          'sql                    @shBrushSql.js',
          'vb vbnet               @shBrushVb.js',
          'xml xhtml xslt html    @shBrushXml.js'
        ));
        SyntaxHighlighter.all();
    </script>
     
 
</e.what()<<'\n';></std::any_cast<int></std::any_cast<double></std::any_cast<int>
```
将输出:

```
2
    3
    bad any_cast
    no value
```
std::any 虽然能很方便地保存任意类型的对象,但是从 any 中取出原来保存的对象时需要调用 `any_cast<T>`,any _ cast 要求传入原始的精确的类型,如果类型不一致将会抛出一个 std::bad _ any _ cast 的异常,由于这一对类型强烈渴求的特点,使得 any 在做类型擦出时多有不便。

很多人对于 any 能保存任意对象的特点非常喜欢,所以喜欢到处用 any,这会导致 any 的滥用,因为 any 保存对象时会有堆内存分配,如果在性能敏感的场景下使用势必会得不偿失。对于 any 这个特性建议谨慎使用,类型擦除可以优先使用 std:: variant(这个特性我们稍后会介绍)。

####  std::optional

optional 体现了一个“可能值”的概念,即某一个值可能被初始化也可能没有被初始化。它的用法也很简单:

```
std::optional<double> divide_d(double a,
double b)
    {
        if(b==0)
            return {};
     
        return a/b;
    }
     
    void test_optional()
    {
        std::optional<double> r = divide_d(6, 2);
        if(r.has_value())
            std::cout<<"has init"<<'\n';
     
        if(r)
            std::cout<<*r<<'\n';
     
        std::optional<double> r1 = divide_d(6, 0);
     
        if(r1==std::nullopt)
            std::cout<<"not init"<<'\n';
    }</double></double></double>
```
optional 很适合不方便返回不合法值的场景,比如本来要返回一个 int 值,但函数内部发现输入参数有问题,这时应该返回一个 int 值告诉调用者,但返回什么值都不合适,这时就用 optional 来表达一个可能的值让调用者知道如果这个返回值没有初始化的话,那说明这个接口调用是失败的。还有一些情况,比如解析 xml 或者其他协议的文本时,有些节点是可能不存在的,这时用 optional 就非常合适了,刚好表达了“可能值”这个概念。

####  std:: variant 

varaint 你可以把它看成是一个类型安全的 union,也可以看成是一个类型容器,比较适合做类型擦除。它的用法相比之前的 optional 和 any 要复杂一些:

```
void test_variant()
    {
        std::variant<int, std::string=""> v, w;
        v = 1;
        int i = std::get<int>(v);
     
        w = "abc";
        auto s = std::get<std::string>(w);
     
        try
        {
            std::get<int>(w);
        }
        catch(const std::bad_variant_access& e)
        {
            std::cout<<e.what()<<'\n'; }="" std::vector<std::variant<int,="" std::string="">> vec;
        vec.push_back(v);
        vec.push_back(w);
    }</e.what()<<'\n';></int></std::string></int></int,>
```
从上面的例子中可以看到 variant 可以保存不同类型的对象,不过这些对象的类型必须是在 variant 定义的类型范围之内,并且不允许有重复类型存在。由于 variant 做了类型擦除,所以我们可以借助它将不同类型的对象放到容器中。

上面的例子中是通过 std::get 来访问 variant 的,在很多时候我们希望通过一种更加泛化的方式来访问 variant,而不是显示指定类型来访问。C++ 17 提供了泛化的专门用来访问 variant 的方法 std::visit,通过 std::visit 我们可以很方便地访问 variant 对象。下面是 std::visit 访问 variant 的方法:

```
void test_variant()
{
    std::variant<int, std::string=""> v, w;
    v = 1;
    w = "abc";
 
    std::vector<std::variant<int, std::string="">> vec;
    vec.push_back(v);
    vec.push_back(w);
    for(auto& item : vec)
    {
        std::visit([](auto&& arg){std::cout<<arg;}, item);="" std::visit([](auto&&="" arg)="" {="" using="" t="std::decay_t" <decltype(arg)="">;
            if constexpr(std::is_same_v<t, int="">)
               std::cout<<"int value";
            else if constexpr (std::is_same_v<t, std::string="">)
               std::cout<<"string value";
            else
               static_assert("no matching");
        }, item);
    }
}</t,></t,></arg;},></std::variant<int,></int,>
```
std::visit 可以直接访问 variant 内部的对象,因为使用了 auto lambda,对于 auto&& arg 来说我们不清楚这个 arg 的类型到底是什么,借助 C++ 17 的 if constexpr,我们可以清楚的知道 arg 具体是什么类型了。

std:: variant 的构造没有堆内存分配,相比 std::any 来说性能更好,而且也不像 any 那样访问内部对象时对类型那么渴求,所以一般情况下应该用 variant 来代替 any 做类型擦除。当然 variant 也有一个不足之处,你需要实现定义所要擦除的类型,有可能需要擦除的类型在开始的时候是无法预知的。

####  std::filesystem

std::filesystem 和 boost::filesystem 用法差不多,主要是用来对路径、文件和目录进行查询和操作。用法也比较简单:

```
namespace fs = std::filesystem;
std::string path = "C:\\test";
for(auto& p : fs::directory_iterator("test"))
        std::cout<<p<<'\n';< pre="">
    <script type="text/javascript">
        function path()
        {
          var args = arguments,
              result = []
              ;
                
          for(var i = 0; i < args.length; i++)
              result.push(args[i].replace('@', '/cms/js/syntax/scripts/'));
                
          return result
        };
          
        SyntaxHighlighter.autoloader.apply(null, path(
          'applescript            @shBrushAppleScript.js',
          'actionscript3 as3      @shBrushAS3.js',
          'bash shell             @shBrushBash.js',
          'coldfusion cf          @shBrushColdFusion.js',
          'cpp c                  @shBrushCpp.js',
          'c# c-sharp csharp      @shBrushCSharp.js',
          'css                    @shBrushCss.js',
          'delphi pascal          @shBrushDelphi.js',
          'diff patch pas         @shBrushDiff.js',
          'erl erlang             @shBrushErlang.js',
          'groovy                 @shBrushGroovy.js',
          'java                   @shBrushJava.js',
          'jfx javafx             @shBrushJavaFX.js',
          'js jscript javascript  @shBrushJScript.js',
          'perl pl                @shBrushPerl.js',
          'php                    @shBrushPhp.js',
          'text plain             @shBrushPlain.js',
          'py python              @shBrushPython.js',
          'ruby rails ror rb      @shBrushRuby.js',
          'sass scss              @shBrushSass.js',
          'scala                  @shBrushScala.js',
          'sql                    @shBrushSql.js',
          'vb vbnet               @shBrushVb.js',
          'xml xhtml xslt html    @shBrushXml.js'
        ));
        SyntaxHighlighter.all();
    </script>
     
 
</p<<'\n';<>
```
### 让代码更加简洁便利的特性
####  nested namespaces

C++ 中如果有多重命名空间的时候,需要嵌套多层,写法上比较难看,现在 C++ 17 让多重命名空间的写法变得简单多了。下面是一个 C++ 11/14 和 C++ 17 多重命名空间写法上的对比:

C++ 17 之前的写法

```
namespace A {
          namespace B {
             namespace C {
                struct Foo { };
                //...
           }
       }
    }
 
    //C++17的写法
    namespace A::B::C {
          struct Foo { };
          //...
    }
```
C++ 17 的写法摆脱了冗长的嵌套和重复,孰繁熟简一目了然。

####  single param staticassert

C++ 11/14 中 static _ assert 断言的时候,必须要填两个参数,前面填条件,后面填断言失败时的提示信息,这在写一些测试代码的时候不够方便,现在 C++ 17 支持了单参数的 static _ assert,当你不想写任何编译期断言失败提示时,就可以不写交给编译器,让编译器告诉用户哪里断言失败了,代码可以写得更简洁了。

```
static_assert(false, "something wrong");
static_assert(false); //省略提示信息,让编译器告诉用户
```

####  if init

C++ 17 允许在 if 条件表达式中初始化变量了,这会让代码写起来更方便,比如在 C++ 11 中我们这样写:

```
std::map<int, bool=""> mp;
    auto pair = mp.insert({1, false});
    if(pair.second)
    {
        std::cout<< (*pair.first).first <<'\n';
    }
    else
    {
        std::cout<<"duplicate"<<'\n';
    }</int,>
```
我们要先定义一个变量,再判断这个变量值做处理。在 C++ 17 中,这个变量可以放到 if 的条件中定义了,不需要单独拿出来定义,像这样:

```
std::map<int, bool=""> mp;
    if(auto pair = mp.insert({1, false}); pair.second)
    {
        std::cout<< (*pair.first).first <<'\n';
    }
    else
{
        std::cout<<"duplicate"<<'\n';
    }</int,>
```
if 表达式中,先定义了一个变量 pair,这个 pair 随后用来做判断条件。注意 if init 中定义的变量作用域就在这个 if 语句中,出了这个 if 语句,定义的变量 pair 就会析构。我们可以利用这个特性来实现自动加锁保护 if 代码段。

```
std::mutex mtx;
    std::vector<int> v;
 
    if(std::lock_guard<std::mutex> lock(mtx);v.empty())
    {
        v.push_back(1);
    }</std::mutex></int>
```
上面的代码利用了 if init 定义的变量的作用域实现了对 if 语句自动加解锁。在 C++ 11 中你需要这样写:

```
std::mutex mtx;
    std::vector<int> v;
 
    {
        std::lock_guard<std::mutex> lock(mtx);
        if(v.empty())
        {
            v.push_back(1);
        }
    }</std::mutex></int>
```
相比之下,C++ 17 利用 if init 的写法更加简洁和紧凑,在语意上更加容易理解。

####  deduction guide
deduction guide 可以根据参数自动推导出对应的类型,这可以让我们的代码变得更加简洁,看下面的写法:

```
std::pair p(1, 1.5);      //推导为std::pair<int, double="">
std::tuple t(1, 2, 2.5);  //推导为std::tuple<int, int,="" double="">
std::vector v{1,2,3};     //推导为std::vector<int>
std::array ar{1,2};       //推导为std::array<int, 2=""></int,></int></int,></int,>
```
从上面的例子中可以看到隐式的 deduction guide 可以让我们的代码写得更加简洁,不用再写模版参数等细节了,有一种写动态语言的感觉。

除了隐式的 deduction guide,还有一种显式的 deduction guide,作用和隐式 deduction guide 差不多,也是让写法变得更简洁。下面是显式 deduction guide 的例子:

```
template<typename t="">
    struct Dummy { T t; };
 
    Dummy(double) -> Dummy<double>;
    Dummy(std::pair<int, int="">) -> Dummy<std::pair<int, int="">>;
 
    void test()
    {
        Dummy dm{2.5};
        Dummy<int> dm1{2};
 
        std::pair<int, int=""> pr{1,2};
        Dummy dm2{pr};
    }</int,></int></std::pair<int,></int,></double></typename>
```
如果没有通过 Dummy(double) -> `Dummy<double>` 显式地做 deduction guide,我们在定义 Dummy 的时候是需要显式带着模版参数的。也许有人觉得就为了省掉一个模版参数却要多定义一个显式的 deduction guide,似乎还变麻烦了。其实显式 deduction guide 主要是为了简化定义可变模版参数的变量,之前介绍过通过 std::visit 来访问 variant,通过 auto lambda 和 if constexpr 来访问 variant 的做法不是很方便,我们可以通过可变模板和显式 deduction guide 的来实现一个访问 variant 的更好的方法。

```
template<class... ts=""> struct overloaded : Ts... { using Ts::operator()...; };
    template<class... ts=""> overloaded(Ts...) -> overloaded<ts...>;
 
    void visit_variant()
    {
        std::variant<int, double,="" std::string=""> v, w;
        v = 1;
        w = "abc";
 
        std::vector<std::variant<int, std::string="">> vec;
        vec.push_back(v);
        vec.push_back(w);
     
        for (auto& it: vec) {
            std::visit(overloaded {
                    [](auto arg) { std::cout << arg << '\n'; },
                    [](double arg) { std::cout << std::fixed << arg << '\n'; },
                    [](const std::string& arg) { std::cout << arg << '\n'; },
            }, it);
        }
    }</std::variant<int,></int,></ts...></class...></class...>
```
上面的例子中先通过可变模版参数的 CRTP 获得参数的 operater(),然后通过显式的 deduction guide 来省去定义含可变模版参数 overloaded 的变量时,需指定变参类型的麻烦,让代码变得非常简洁。这里不仅仅是让代码变得简洁,还直接避免了一个难题,因为 overloaded 参数是 lambda,它是一个匿名类型,你无法获取 lambda 的类型并实例化 overloaded 模版。当然我们还可以像 boost 中定一个函数对象的方式来访问 variant,像下面这样:

```
struct visitor
    {
        void operator()(int arg){ std::cout << arg << '\n'; }
        void operator()(double arg){ std::cout << arg << '\n'; }
        void operator()(const std::string& arg){ std::cout << arg << '\n'; }
    };
     
    void visit_variant()
    {
        std::variant<int, double,="" std::string=""> v, w;
        v = 1;
        w = "abc";
 
        std::vector<std::variant<int, std::string="">> vec;
        vec.push_back(v);
        vec.push_back(w);
     
        visitor vt;
        for (auto& it: vec) {
            std::visit(vt, it);

    }</std::variant<int,></int,>
```
####  capture *this

C++ 17 中的 capture *this 允许我们拷贝 this 对象,在 C++ 17 之前,如果我们需要拷贝 this 对象需要这样写:

```
[=,copy=*this]{ }
```
显得比较烦琐,C++ 17 里就变得很简洁了,直接写:

```
[=,*this]{ }
```
也许有人会有疑惑,我为什么需要拷贝 this 对象呢,因为在有些场景下,执行 lambda 的时候 this 对象内部的状态可能会发生变化,我不希望执行 lambda 的时候这个状态变了,这时就希望通过拷贝 this 对象来确保之前的状态不会变。

### 新增加的算法
####  std::search

C++ 17 中新增了一些实用算法,std::search 就是在一个 range 中查找一个子 range,这很容易让我们联想到查找公共子串的算法。下面是 std::search 的用法:

```
void test_new_alg()
{
    std::string in = "Lorem ipsum dolor sit amet, consectetur adipiscing elit,"
            " sed do eiusmod tempor incididunt ut labore et dolore magna aliqua";
 
    std::string needle = "pisci";
    auto it = std::search(in.begin(), in.end(), needle.begin(), needle.end());
 
    if (it != in.end())
        std::cout << "Found at " << (it - in.begin())<<'\n';
}
```
也许有人会疑惑,这个 std::search 和 std::find _ first _ of 算法有什么不同吗,二者最大的不同是:std::search 是按一个子 range 范围查找的,而 std::find _ first _ of 是按 range 中的任意一个元素去超找的。

####  并行算法

C++ 17 中增加了很多并行算法,主要在 algorithm,numeric 和 memory 下面,见图1。

<img src="http://ipad-cms.csdn.net/cms/attachment/201709/59a91c08a3477.jpg" alt="图1  C++ 17并行算法" title="图1  C++ 17并行算法" />

图1  C++ 17 并行算法

由于目前的编译器还没有完全支持 C++ 17 的并行算法,而且并行算法比较多,这里仅用 cppreference.com 上的一个例子来展示并行算法的用法:

```
std::vector<double> v(10'000'007, 0.5);
  
    {
        auto t1 = std::chrono::high_resolution_clock::now();
        double result = std::accumulate(v.begin(), v.end(), 0.0);
        auto t2 = std::chrono::high_resolution_clock::now();
        std::chrono::duration<double, std::milli=""> ms = t2 - t1;
        std::cout << std::fixed << "std::accumulate result " << result
                  << " took " << ms.count() << " ms\n";
    }
  
    {
        auto t1 = std::chrono::high_resolution_clock::now();
        double result = std::reduce(std::execution::par, v.begin(), v.end());
        auto t2 = std::chrono::high_resolution_clock::now();
        std::chrono::duration<double, std::milli=""> ms = t2 - t1;
        std::cout << "std::reduce result "
                  << result << " took " << ms.count() << " ms\n";
    }
 
    //output
    std::accumulate result 5000003.50000 took 12.7365 ms
    std::reduce result 5000003.50000 took 5.06423 ms</double,></double,></double>
```
可以看到并行算法比非并行算法效率提升了两倍多,可以看到 C++ 17 的并行算法将进一步提高程序的计算效率,将进一步提升 C++ 在高性能计算领域的能力!

### 总结
本文主要介绍了来自 boost 库的特性如 any、optional、variant 和 filesystem,这些特性在 boost 中存在已久并且挺实用的,加入到标准库中作为一些便利地工具;还介绍了让代码变得简洁便利的一些特性,比如 nested namespace、if init、dedution guide 等特性确实能让我们的代码写得更加简洁优雅;最后介绍了新增的算法,尤其是并行算法可以大幅提升程序的计算效率,C++ 17 的并行算法值得好好研究,这些并行算法在高性能领域非常有潜力。