Boost C++ 应用开发秘籍第二版(四)

原文:annas-archive.org/md5/8a1821d22bcd421390c328e6f1d92500

译者:飞龙

协议:CC BY-NC-SA 4.0

第七章:操作字符串

在本章中,我们将涵盖:

  • 更改大小写和不区分大小写比较

  • 使用正则表达式匹配字符串

  • 使用正则表达式搜索和替换字符串

  • 使用安全的 printf 样式函数格式化字符串

  • 替换和删除字符串

  • 用两个迭代器表示一个字符串

  • 使用对字符串类型的引用

介绍

整个章节都致力于不同方面的更改、搜索和表示字符串。我们将看到如何使用 Boost 库轻松完成一些常见的与字符串相关的任务。这一章很容易;它涉及非常常见的字符串操作任务。所以,让我们开始吧!

更改大小写和不区分大小写比较

这是一个非常常见的任务。我们有两个非 Unicode 或 ANSI 字符字符串:

#include <string> 
std::string str1 = "Thanks for reading me!"; 
std::string str2 = "Thanks for reading ME!"; 

我们需要以不区分大小写的方式进行比较。有很多方法可以做到这一点,让我们看看 Boost 的方法。

准备工作

这里我们只需要基本的std::string知识。

如何做…

以下是进行不区分大小写比较的不同方法:

  1. 最简单的方法是:
#include <boost/algorithm/string/predicate.hpp> 

const bool solution_1 = (
     boost::iequals(str1, str2)
);
  1. 使用 Boost 谓词和标准库方法:
#include <boost/algorithm/string/compare.hpp> 
#include <algorithm> 

const bool solution_2 = (
    str1.size() == str2.size() && std::equal(
        str1.begin(),
        str1.end(),
        str2.begin(),
        boost::is_iequal()
    )
);
  1. 制作两个字符串的小写副本:
#include <boost/algorithm/string/case_conv.hpp> 

void solution_3() {
    std::string str1_low = boost::to_lower_copy(str1);
    std::string str2_low = boost::to_lower_copy(str2);
    assert(str1_low == str2_low);
}
  1. 制作原始字符串的大写副本:
#include <boost/algorithm/string/case_conv.hpp> 

void solution_4() {
    std::string str1_up = boost::to_upper_copy(str1);
    std::string str2_up = boost::to_upper_copy(str2);
    assert(str1_up == str2_up);
}
  1. 将原始字符串转换为小写:
#include <boost/algorithm/string/case_conv.hpp> 

void solution_5() {
    boost::to_lower(str1);
    boost::to_lower(str2);
    assert(str1 == str2);
}

它是如何工作的…

第二种方法并不明显。在第二种方法中,我们比较字符串的长度。如果它们长度相同,我们使用boost::is_iequal谓词的实例逐个字符比较字符串,该谓词以不区分大小写的方式比较两个字符。

Boost.StringAlgorithm库在方法或类的名称中使用i,如果该方法是不区分大小写的。例如,boost::is_iequalboost::iequalsboost::is_iless等。

还有更多…

Boost.StringAlgorithm库的每个函数和函数对象都接受std::locale。默认情况下(在我们的示例中),方法和类使用默认构造的std::locale。如果我们大量使用字符串,一次构造std::locale变量并将其传递给所有方法可能是一个很好的优化。另一个很好的优化是通过std::locale::classic()使用C语言环境(如果您的应用逻辑允许):

  // On some platforms std::locale::classic() works 
  // faster than std::locale().
  boost::iequals(str1, str2, std::locale::classic()); 

没有人禁止您同时使用这两种优化。

不幸的是,C++17 没有来自Boost.StringAlgorithm的字符串函数。所有的算法都快速可靠,所以不要害怕在代码中使用它们。

另请参阅

  • Boost String Algorithms 库的官方文档可以在boost.org/libs/algorithm/string找到

  • 请参阅 Andrei Alexandrescu 和 Herb Sutter 的C++编程标准一书,了解如何使用几行代码制作不区分大小写的字符串的示例

使用正则表达式匹配字符串

让我们做一些有用的事情!当用户的输入必须使用一些正则表达式进行检查时,这是一个常见情况。问题在于有很多正则表达式语法,使用一种语法编写的表达式在其他语法中处理得不好。另一个问题是,长的正则表达式不那么容易编写。

因此,在这个示例中,我们将编写一个支持不同正则表达式语法并检查输入字符串是否匹配指定正则表达式的程序。

入门

这个示例需要基本的标准库知识。了解正则表达式语法可能会有所帮助。

需要将示例链接到boost_regex库。

如何做…

这个正则表达式匹配器示例由main()函数中的几行代码组成:

  1. 要实现它,我们需要以下标头:
#include <boost/regex.hpp> 
#include <iostream> 
  1. 在程序开始时,我们需要输出可用的正则表达式语法:
int main() { 
    std::cout  
        << "Available regex syntaxes:\n" 
        << "\t[0] Perl\n" 
        << "\t[1] Perl case insensitive\n" 
        << "\t[2] POSIX extended\n" 
        << "\t[3] POSIX extended case insensitive\n" 
        << "\t[4] POSIX basic\n" 
        << "\t[5] POSIX basic case insensitive\n\n" 
        << "Choose regex syntax: "; 
  1. 现在,根据所选择的语法正确设置标志:
    boost::regex::flag_type flag;
    switch (std::cin.get()) 
    {
    case '0': flag = boost::regex::perl;
        break;

    case '1': flag = boost::regex::perl|boost::regex::icase;
        break;

    case '2': flag = boost::regex::extended;
        break;

    case '3': flag = boost::regex::extended|boost::regex::icase;
        break;

    case '4': flag = boost::regex::basic;
        break;

    case '5': flag = boost::regex::basic|boost::regex::icase;
        break;
    default:
        std::cout << "Incorrect number of regex syntax. Exiting...\n";
        return 1;
    }

    // Disabling exceptions.
    flag |= boost::regex::no_except;
  1. 我们现在在循环中请求正则表达式模式:
    // Restoring std::cin.
    std::cin.ignore();
    std::cin.clear();

    std::string regex, str;
    do {
        std::cout << "Input regex: ";
        if (!std::getline(std::cin, regex) || regex.empty()) {
            return 0;
        }

        // Without `boost::regex::no_except`flag this
        // constructor may throw.
        const boost::regex e(regex, flag);
        if (e.status()) {
            std::cout << "Incorrect regex pattern!\n";
            continue;
        }
  1. 在循环中获取要匹配的字符串
        std::cout << "String to match: ";
        while (std::getline(std::cin, str) && !str.empty()) {
  1. 对其应用正则表达式并输出结果:
            const bool matched = boost::regex_match(str, e);
            std::cout << (matched ? "MATCH\n" : "DOES NOT MATCH\n");
            std::cout << "String to match: ";
        } // end of `while (std::getline(std::cin, str))`
  1. 我们将通过恢复std::cin并请求新的正则表达式模式来完成我们的示例:
        // Restoring std::cin.
        std::cin.ignore();
        std::cin.clear();
    } while (1);
} // int main() 

现在,如果我们运行前面的示例,我们将得到以下输出:

 Available regex syntaxes:
 [0] Perl
 [1] Perl case insensitive
 [2] POSIX extended
 [3] POSIX extended case insensitive
 [4] POSIX basic
 [5] POSIX basic case insensitive
Choose regex syntax: 0
 Input regex: (\d{3}[#-]){2}
 String to match: 123-123#
 MATCH
 String to match: 312-321-
 MATCH
 String to match: 21-123-
 DOES NOT MATCH
 String to match: ^Z
 Input regex: \l{3,5}
 String to match: qwe
 MATCH
 String to match: qwert
 MATCH
 String to match: qwerty
 DOES NOT MATCH
 String to match: QWE
 DOES NOT MATCH
 String to match: ^Z

 Input regex: ^Z
 Press any key to continue . . .

工作原理…

所有的匹配都是由boost::regex类完成的。它构造了一个能够进行正则表达式解析和编译的对象。通过flag输入变量将额外的配置选项传递给类。

如果正则表达式不正确,boost::regex会抛出异常。如果传递了boost::regex::no_except标志,它会在status()调用中返回非零以报告错误(就像我们的示例中一样):

        if (e.status()) {
            std::cout << "Incorrect regex pattern!\n";
            continue;
        }

这将导致:

Input regex: (incorrect regex(
Incorrect regex pattern!

通过调用boost::regex_match函数来进行正则表达式匹配。如果匹配成功,它将返回true。可以向regex_match传递其他标志,但为了简洁起见,我们避免了它们的使用。

还有更多…

C++11 几乎包含了所有Boost.Regex类和标志。它们可以在std::命名空间的<regex>头文件中找到(而不是boost::)。官方文档提供了关于 C++11 和Boost.Regex的差异的信息。它还包含一些性能测量,表明Boost.Regex很快。一些标准库存在性能问题,因此在 Boost 和标准库版本之间明智地进行选择。

另请参阅

  • 使用正则表达式搜索和替换字符串示例将为您提供有关Boost.Regex用法的更多信息

  • 您还可以考虑官方文档,以获取有关标志、性能测量、正则表达式语法和 C++11 兼容性的更多信息,网址为boost.org/libs/regex

使用正则表达式搜索和替换字符串

我的妻子非常喜欢通过正则表达式匹配字符串示例。但是,她想要更多,并告诉我,除非我提升这个配方以便能够根据正则表达式匹配替换输入字符串的部分,否则我将得不到食物。

好的,它来了。每个匹配的子表达式(括号中的正则表达式部分)必须从 1 开始获得一个唯一的编号;这个编号将用于创建一个新的字符串。

这就是更新后的程序应该工作的方式:

 Available regex syntaxes:
 [0] Perl
 [1] Perl case insensitive
 [2] POSIX extended
 [3] POSIX extended case insensitive
 [4] POSIX basic
 [5] POSIX basic case insensitive

 Choose regex syntax: 0
 Input regex: (\d)(\d)
 String to match: 00
 MATCH: 0, 0,
 Replace pattern: \1#\2
 RESULT: 0#0
 String to match: 42
 MATCH: 4, 2,
 Replace pattern: ###\1-\1-\2-\1-\1###
 RESULT: ###4-4-2-4-4###

准备工作

我们将重用通过正则表达式匹配字符串示例中的代码。建议在阅读本示例之前先阅读它。

需要链接一个示例到boost_regex库。

如何做到…

这个配方是基于前一个配方的代码。让我们看看必须改变什么:

  1. 不需要包含额外的头文件。但是,我们需要一个额外的字符串来存储替换模式:
    std::string regex, str, replace_string;
  1. 我们用boost::regex_match替换为boost::regex_find并输出匹配的结果:
        std::cout << "String to match: ";
        while (std::getline(std::cin, str) && !str.empty()) {
            boost::smatch results;
            const bool matched = regex_search(str, results, e);
            if (matched)  {
                std::cout << "MATCH: ";
                std::copy(
                    results.begin() + 1, 
                    results.end(), 
                    std::ostream_iterator<std::string>(std::cout, ", ")
                );
  1. 之后,我们需要获取替换模式并应用它:
                std::cout << "\nReplace pattern: ";
                if (
                        std::getline(std::cin, replace_string)
                        && !replace_string.empty())
                {
                    std::cout << "RESULT: " << 
                        boost::regex_replace(str, e, replace_string)
                    ; 
                } else {
                    // Restoring std::cin.
                    std::cin.ignore();
                    std::cin.clear();
                }
            } else { // `if (matched) `
                std::cout << "DOES NOT MATCH";
            }

就是这样!每个人都很开心,我也吃饱了。

工作原理…

boost::regex_search函数不仅返回truefalse值(不像boost::regex_match函数那样),而且还存储匹配的部分。我们使用以下结构输出匹配的部分:

    std::copy( 
        results.begin() + 1,  
        results.end(),  
        std::ostream_iterator<std::string>( std::cout, ", ") 
    ); 

请注意,我们通过跳过第一个结果(results.begin() + 1)输出了结果,这是因为results.begin()包含整个正则表达式匹配。

boost::regex_replace函数执行所有替换并返回修改后的字符串。

还有更多…

regex_*函数有不同的变体,其中一些接收双向迭代器而不是字符串,有些则向迭代器提供输出。

boost::smatchboost::match_results<std::string::const_iterator>typedef。如果您使用的是std::string::const_iterator之外的其他双向迭代器,您应该将您的双向迭代器的类型作为boost::match_results的模板参数。

match_results有一个格式函数,因此我们可以使用它来调整我们的示例,而不是:

std::cout << "RESULT: " << boost::regex_replace(str, e, replace_string); 

我们可以使用以下内容:

std::cout << "RESULT: " << results.format(replace_string); 

顺便说一下,replace_string支持多种格式:

Input regex: (\d)(\d)
 String to match: 12
 MATCH: 1, 2,
 Replace pattern: $1-$2---$&---$$
 RESULT: 1-2---12---$

此处的所有类和函数都存在于 C++11 的<regex>头文件的std::命名空间中。

另请参阅

Boost.Regex的官方文档将为您提供更多关于性能、C++11 标准兼容性和正则表达式语法的示例和信息,网址为boost.org/libs/regex通过正则表达式匹配字符串示例将告诉您Boost.Regex的基础知识。

使用安全的 printf 样式函数格式化字符串

printf系列函数对安全性构成威胁。允许用户将自己的字符串作为类型并格式化说明符是非常糟糕的设计。那么当需要用户定义的格式时,我们该怎么办?我们应该如何实现以下类的成员函数std::string to_string(const std::string& format_specifier) const;

class i_hold_some_internals 
{
    int i;
    std::string s;
    char c;
    // ...
}; 

准备工作

对标准库的基本知识就足够了。

如何做到…

我们希望允许用户为字符串指定自己的输出格式:

  1. 为了以安全的方式进行操作,我们需要以下头文件:
#include <boost/format.hpp>
  1. 现在,我们为用户添加一些注释:
    // `fmt` parameter may contain the following:
    // $1$ for outputting integer 'i'.
    // $2$ for outputting string 's'.
    // $3$ for outputting character 'c'.
    std::string to_string(const std::string& fmt) const {
  1. 是时候让所有部分都运行起来了:
        boost::format f(fmt);
        unsigned char flags = boost::io::all_error_bits;
        flags ^= boost::io::too_many_args_bit;
        f.exceptions(flags);
        return (f % i % s % c).str();
    }

就是这样。看一下这段代码:

int main() {
    i_hold_some_internals class_instance;

    std::cout << class_instance.to_string(
        "Hello, dear %2%! "
        "Did you read the book for %1% %% %3%\n"
    );

    std::cout << class_instance.to_string(
        "%1% == %1% && %1%%% != %1%\n\n"
    );
}

假设class_instance有一个成员i等于100,一个成员s等于"Reader",一个成员c等于'!'。然后,程序将输出如下内容:

 Hello, dear Reader! Did you read the book for 100 % !
 100 == 100 && 100% != 100

它是如何工作的…

boost::format类接受指定结果字符串格式的字符串。参数通过operator%传递给boost::format。在指定字符串格式中,%1%%2%%3%%4%等值会被传递给boost::format的参数替换。

我们还禁用了异常,以防格式字符串包含的参数少于传递给boost::format的参数:

    boost::format f(format_specifier);
    unsigned char flags = boost::io::all_error_bits;
    flags ^= boost::io::too_many_args_bit;

这样做是为了允许一些这样的格式:

    // Outputs 'Reader'.
    std::cout << class_instance.to_string("%2%\n\n");

还有更多…

在格式不正确的情况下会发生什么?

没有什么可怕的,会抛出一个异常:

    try {
        class_instance.to_string("%1% %2% %3% %4% %5%\n");
        assert(false);
    } catch (const std::exception& e) {
        // boost::io::too_few_args exception is catched.
        std::cout << e.what() << '\n';
    }

前一个代码片段通过控制台输出了以下行:

 boost::too_few_args: format-string referred to more arguments than
    were passed

C++17 没有std::formatBoost.Format库不是一个非常快的库。尽量不要在性能关键的部分大量使用它。

另请参阅

官方文档包含了有关Boost.Format库性能的更多信息。在boost.org/libs/format上还有更多关于扩展 printf 格式的示例和文档。



替换和擦除字符串

我们需要在字符串中擦除某些内容,替换字符串的一部分,或者擦除某些子字符串的第一个或最后一个出现的情况非常常见。标准库允许我们做更多的部分,但通常需要编写太多的代码。

我们在更改大小写和不区分大小写比较示例中看到了Boost.StringAlgorithm库的实际应用。让我们看看当我们需要修改一些字符串时,它如何简化我们的生活:

#include <string> 
const std::string str = "Hello, hello, dear Reader."; 

准备工作

这个示例需要对 C++有基本的了解。

如何做到…

这个示例展示了Boost.StringAlgorithm库中不同的字符串擦除和替换方法的工作原理:

  1. 擦除需要#include <boost/algorithm/string/erase.hpp>头文件:
#include <boost/algorithm/string/erase.hpp>

void erasing_examples() {
    namespace ba = boost::algorithm;
    using std::cout;

    cout << "\n erase_all_copy :" << ba::erase_all_copy(str, ",");
    cout << "\n erase_first_copy:" << ba::erase_first_copy(str, ",");
    cout << "\n erase_last_copy :" << ba::erase_last_copy(str, ",");
    cout << "\n ierase_all_copy :" << ba::ierase_all_copy(str, "hello");
    cout << "\n ierase_nth_copy :" << ba::ierase_nth_copy(str, ",", 1);
}

这段代码输出如下内容:

 erase_all_copy   :Hello hello dear Reader.
 erase_first_copy :Hello hello, dear Reader.
 erase_last_copy  :Hello, hello dear Reader.
 ierase_all_copy   :, , dear Reader.
 ierase_nth_copy  :Hello, hello dear Reader.
  1. 替换需要<boost/algorithm/string/replace.hpp>头文件:
#include <boost/algorithm/string/replace.hpp>

void replacing_examples() {
    namespace ba = boost::algorithm;
    using std::cout;

    cout << "\n replace_all_copy :" 
        << ba::replace_all_copy(str, ",", "!");

    cout << "\n replace_first_copy :"
        << ba::replace_first_copy(str, ",", "!");

    cout << "\n replace_head_copy :"
        << ba::replace_head_copy(str, 6, "Whaaaaaaa!");
}

这段代码输出如下内容:

 replace_all_copy :Hello! hello! dear Reader.
 replace_first_copy :Hello! hello, dear Reader.
 replace_head_copy :Whaaaaaaa! hello, dear Reader.

它是如何工作的…

所有示例都是自解释的。唯一不明显的是replace_head_copy函数。它接受要替换的字节数作为第二个参数,替换字符串作为第三个参数。因此,在前面的示例中,Hello被替换为Whaaaaaaa!

还有更多…

还有一些可以就地修改字符串的方法。它们不以_copy结尾,返回void。所有不区分大小写的方法(以i开头的方法)都接受std::locale作为最后一个参数,并使用默认构造的 locale 作为默认参数。

您经常使用不区分大小写的方法并且需要更好的性能吗?只需创建一个持有std::locale::classic()std::locale变量,并将其传递给所有算法。在小字符串上,大部分时间都被std::locale构造所消耗,而不是算法:

#include <boost/algorithm/string/erase.hpp>

void erasing_examples_locale() {
    namespace ba = boost::algorithm;

    const std::locale loc = std::locale::classic();

    const std::string r1
        = ba::ierase_all_copy(str, "hello", loc);

    const std::string r2
        = ba::ierase_nth_copy(str, ",", 1, loc);

    // ...
}

C++17 没有Boost.StringAlgorithm方法和类。然而,它有一个std::string_view类,可以在没有内存分配的情况下使用子字符串。您可以在本章的下两个配方中找到更多关于类似std::string_view的类的信息。

另请参阅

  • 官方文档包含大量示例和所有方法的完整参考boost.org/libs/algorithm/string

  • 有关Boost.StringAlgorithm库的更多信息,请参见本章的更改大小写和不区分大小写比较配方

用两个迭代器表示一个字符串

有时我们需要将一些字符串拆分成子字符串并对这些子字符串进行操作。在这个配方中,我们想将字符串拆分成句子,计算字符和空格,当然,我们想使用 Boost 并尽可能高效。

准备工作

对于这个配方,您需要一些标准库算法的基本知识。

如何做…

使用 Boost 非常容易:

  1. 首先,包括正确的头文件:
#include <iostream>
#include <boost/algorithm/string/split.hpp>
#include <boost/algorithm/string/classification.hpp>
#include <algorithm>
  1. 现在,让我们定义我们的测试字符串:
int main() {
    const char str[] =
        "This is a long long character array."
        "Please split this character array to sentences!"
        "Do you know, that sentences are separated using period, "
        "exclamation mark and question mark? :-)"
    ;
  1. 我们为我们的分割迭代器制作了一个typedef
    typedef boost::split_iterator<const char*> split_iter_t;
  1. 构造该迭代器:
    split_iter_t sentences = boost::make_split_iterator(str,
        boost::algorithm::token_finder(boost::is_any_of("?!."))
    );
  1. 现在,我们可以在匹配之间进行迭代:
    for (unsigned int i = 1; !sentences.eof(); ++sentences, ++i) {
        boost::iterator_range<const char*> range = *sentences;
        std::cout << "Sentence #" << i << " : \t" << range << '\n';
  1. 计算字符的数量:
        std::cout << range.size() << " characters.\n";
  1. 并计算空格:
        std::cout 
            << "Sentence has " 
            << std::count(range.begin(), range.end(), ' ') 
            << " whitespaces.\n\n";
    } // end of for(...) loop
} // end of main()

就是这样。现在,如果我们运行一个示例,它将输出:

 Sentence #1 : This is a long long character array
 35 characters.
 Sentence has 6 whitespaces.

 Sentence #2 : Please split this character array to sentences
 46 characters.
 Sentence has 6 whitespaces.

 Sentence #3 : Do you know, that sentences are separated using dot,
 exclamation mark and question mark
 90 characters.
 Sentence has 13 whitespaces.

 Sentence #4 : :-)
 4 characters.
 Sentence has 1 whitespaces.

它是如何工作的…

这个配方的主要思想是我们不需要从子字符串构造std::string。我们甚至不需要一次性对整个字符串进行标记。我们所需要做的就是找到第一个子字符串,并将其作为一对迭代器返回到子字符串的开头和结尾。如果我们需要更多的子字符串,找到下一个子字符串并返回该子字符串的一对迭代器。

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

现在,让我们更仔细地看看boost::split_iterator。我们使用boost::make_split_iterator函数构造了一个,它将range作为第一个参数,二进制查找谓词(或二进制谓词)作为第二个参数。当解引用split_iterator时,它将第一个子字符串作为boost::iterator_range<const char*>返回,它只是保存一对指针并有一些方法来处理它们。当我们递增split_iterator时,它会尝试找到下一个子字符串,如果没有找到子字符串,split_iterator::eof()将返回true

默认构造的分割迭代器表示eof()。因此,我们可以将循环条件从!sentences.eof()重写为sentences != split_iter_t()。您还可以使用分割迭代器与算法,例如:std::for_each(sentences, split_iter_t(), [](auto range){ /**/ });

还有更多…

boost::iterator_range类广泛用于所有 Boost 库。即使在您自己的代码中,当需要返回一对迭代器或者函数需要接受/处理一对迭代器时,您可能会发现它很有用。

boost::split_iterator<>boost::iterator_range<>类接受前向迭代器类型作为模板参数。因为在前面的示例中我们使用字符数组,所以我们提供了const char*作为迭代器。如果我们使用std::wstring,我们需要使用boost::split_iterator<std::wstring::const_iterator>boost::iterator_range<std::wstring::const_iterator>类型。

C++17 中既没有iterator_range也没有split_iterator。然而,正在讨论接受类似iterator_range的类,可能会有名为std::span的名称。

boost::iterator_range类没有虚函数和动态内存分配,它非常快速和高效。然而,它的输出流操作符<<对字符数组没有特定的优化,因此流操作可能会很慢。

boost::split_iterator类中有一个boost::function类,因此为大型函数构造它可能会很慢。迭代只会增加微小的开销,即使在性能关键的部分,你也不会感觉到。

另请参阅

  • 下一个示例将告诉您boost::iterator_range<const char*>的一个很好的替代品

  • Boost.StringAlgorithm的官方文档可能会为您提供有关类的更详细信息以及大量示例的信息,网址为boost.org/libs/algorithm/string

  • 关于boost::iterator_range的更多信息可以在这里找到:boost.org/libs/range;它是Boost.Range库的一部分,本书中没有描述,但您可能希望自行研究它

使用对字符串类型的引用

这个示例是本章中最重要的示例!让我们看一个非常常见的情况,我们编写一些接受字符串并返回在startsends参数中传递的字符值之间的字符串部分的函数:

#include <string>
#include <algorithm>

std::string between_str(const std::string& input, char starts, char ends) {
    std::string::const_iterator pos_beg 
        = std::find(input.begin(), input.end(), starts);
    if (pos_beg == input.end()) {
        return std::string();
    }
    ++ pos_beg;

    std::string::const_iterator pos_end 
        = std::find(pos_beg, input.end(), ends);

    return std::string(pos_beg, pos_end);
}

你喜欢这个实现吗?在我看来,这个实现很糟糕。考虑对它的以下调用:

between_str("Getting expression (between brackets)", '(', ')'); 

在这个示例中,从"Getting expression (between brackets)"构造了一个临时的std::string变量。字符数组足够长,因此在std::string构造函数内可能会调用动态内存分配,并将字符数组复制到其中。然后,在between_str函数的某个地方,将构造新的std::string,这可能还会导致另一个动态内存分配和复制。

因此,这个简单的函数可能会,并且在大多数情况下会:

  • 调用动态内存分配(两次)

  • 复制字符串(两次)

  • 释放内存(两次)

我们能做得更好吗?

准备工作

这个示例需要对标准库和 C++有基本的了解。

如何做…

在这里我们实际上并不需要std::string类,我们只需要一些轻量级的类,它不管理资源,只有一个指向字符数组和数组大小的指针。Boost 有boost::string_view类可以满足这个需求。

  1. 要使用boost::string_view类,请包含以下头文件:
#include <boost/utility/string_view.hpp>
  1. 更改方法的签名:
boost::string_view between(
    boost::string_view input,
    char starts,
    char ends)
  1. 在函数体内的任何地方将std::string更改为boost::string_view
{
    boost::string_view::const_iterator pos_beg 
        = std::find(input.cbegin(), input.cend(), starts);
    if (pos_beg == input.cend()) {
        return boost::string_view();
    }
    ++ pos_beg;

    boost::string_view::const_iterator pos_end 
        = std::find(pos_beg, input.cend(), ends);
    // ...
  1. boost::string_view构造函数接受大小作为第二个参数,因此我们需要稍微更改代码:
    if (pos_end == input.cend()) {
        return boost::string_view(pos_beg, input.end() - pos_beg);
    }

    return boost::string_view(pos_beg, pos_end - pos_beg);
}

就是这样!现在我们可以调用between("Getting expression (between brackets)", '(', ')'),而且它将在没有任何动态内存分配和字符复制的情况下工作。而且我们仍然可以将其用于std::string

   between(std::string("(expression)"), '(', ')')

工作原理…

如前所述,boost::string_view只包含一个指向字符数组的指针和数据大小。它有很多构造函数,可以以不同的方式初始化:

    boost::string_view r0("^_^");

    std::string O_O("O__O");
    boost::string_view r1 = O_O;

    std::vector<char> chars_vec(10, '#');
    boost::string_view r2(&chars_vec.front(), chars_vec.size());

boost::string_view类具有container类所需的所有方法,因此可以与标准库算法和 Boost 算法一起使用:

#include <boost/algorithm/string/case_conv.hpp>
#include <boost/algorithm/string/replace.hpp>
#include <boost/lexical_cast.hpp>
#include <iterator>
#include <iostream>

void string_view_algorithms_examples() {
    boost::string_view r("O_O");
    // Finding single symbol.
    std::find(r.cbegin(), r.cend(), '_');

    // Will print 'o_o'.
    boost::to_lower_copy(std::ostream_iterator<char>(std::cout), r);
    std::cout << '\n';

    // Will print 'O_O'.
    std::cout << r << '\n';

    // Will print '^_^'.
    boost::replace_all_copy(
        std::ostream_iterator<char>(std::cout), r, "O", "^"
    );
    std::cout << '\n';

    r = "100";
    assert(boost::lexical_cast<int>(r) == 100);
}

boost::string_view类实际上并不拥有字符串,因此它的所有方法都返回常量迭代器。因此,我们不能在修改数据的方法中使用它,比如boost::to_lower(r)

在使用boost::string_view时,我们必须额外注意它所引用的数据;它必须存在并且在整个boost::string_view变量的生命周期内都有效。

在 Boost 1.61 之前,没有boost::string_view类,而是使用boost::string_ref类。这些类非常接近。boost::string_view更接近 C++17 的设计,并且具有更好的 constexpr 支持。自 Boost 1.61 以来,boost::string_ref已被弃用。

string_view类是快速和高效的,因为它们从不分配内存,也没有虚函数!在任何可能的地方使用它们。它们被设计为const std::string&const char*参数的即插即用替代品。这意味着你可以替换以下三个函数:

void foo(const std::string& s);
void foo(const char* s);
void foo(const char* s, std::size_t s_size);

用一个单一的:

void foo(boost::string_view s);

还有更多…

boost::string_view类是一个 C++17 类。如果您的编译器兼容 C++17,可以在std::命名空间的<string_view>头文件中找到它。

Boost 和标准库的版本支持对string_view的 constexpr 使用;然而,std::string_view目前具有更多的标记为 constexpr 的函数。

请注意,我们已经通过值接受了string_view变量,而不是常量引用。这是传递boost::string_viewstd::string_view的推荐方式,因为:

  • string_view是一个具有平凡类型的小类。通过值传递它通常会导致更好的性能,因为减少了间接引用,并且允许编译器进行更多的优化。

  • 在其他情况下,当没有性能差异时,编写string_view val比编写const string_view& val更短。

就像 C++17 的std::string_view一样,boost::string_view类实际上是一个typedef

typedef basic_string_view<char, std::char_traits<char> > string_view; 

您还可以在boost::std::命名空间中找到宽字符的以下 typedef:

typedef basic_string_view<wchar_t,  std::char_traits<wchar_t> > wstring_view; 

typedef basic_string_view<char16_t, std::char_traits<char16_t> > u16string_view; 

typedef basic_string_view<char32_t, std::char_traits<char32_t> > u32string_view; 

另请参阅

string_refstring_view的 Boost 文档可以在boost.org/libs/utility找到。

第八章:元编程

在本章中,我们将涵盖:

  • 使用类型向量

  • 操作类型向量

  • 在编译时获取函数的结果类型

  • 制作一个高阶元函数

  • 延迟评估元函数

  • 将所有元组元素转换为字符串

  • 拆分元组

  • 在 C++14 中操作异构容器

介绍

本章专门介绍一些酷而难以理解的元编程方法。这些方法不是为日常使用而设计的,但它们可能对开发通用库有所帮助。

第四章,编译时技巧,已经涵盖了元编程的基础知识。建议阅读以便更好地理解。在本章中,我们将深入探讨如何将多个类型打包在单个类似元组的类型中。我们将创建用于操作类型集合的函数,看看如何改变编译时集合的类型,以及如何将编译时技巧与运行时混合。所有这些都是元编程。

系好安全带,准备好,让我们开始…!

使用类型向量

有时候,希望能够像在容器中一样处理所有模板参数。想象一下,我们正在编写一些东西,比如Boost.Variant

#include <boost/mpl/aux_/na.hpp>

// boost::mpl::na == n.a. == not available
template <
    class T0 = boost::mpl::na,
    class T1 = boost::mpl::na,
    class T2 = boost::mpl::na,
    class T3 = boost::mpl::na,
    class T4 = boost::mpl::na,
    class T5 = boost::mpl::na,
    class T6 = boost::mpl::na,
    class T7 = boost::mpl::na,
    class T8 = boost::mpl::na,
    class T9 = boost::mpl::na
    >
struct variant;

上述代码是所有以下有趣任务开始发生的地方:

  • 我们如何去除所有类型的常量和易失性限定符?

  • 我们如何去除重复类型?

  • 我们如何获得所有类型的大小?

  • 我们如何获得输入参数的最大大小?

所有这些任务都可以很容易地使用Boost.MPL解决。

准备好

需要对第四章的编译时技巧有基本了解才能使用这个示例。在阅读之前要鼓起一些勇气–这个示例中会有很多元编程。

如何做…

我们已经看到了如何在编译时操作类型。为什么我们不能进一步组合多个类型在一个数组中,并对该数组的每个元素执行操作呢?

  1. 首先,让我们将所有类型打包在Boost.MPL类型的容器中:
#include <boost/mpl/vector.hpp>

template <
    class T0, class T1, class T2, class T3, class T4,
    class T5, class T6, class T7, class T8, class T9
>
struct variant {
    typedef boost::mpl::vector<
        T0, T1, T2, T3, T4, T5, T6, T7, T8, T9
    > types;
};
  1. 让我们将我们的示例变得不那么抽象,看看如果我们指定类型会发生什么:
#include <string>
struct declared{ unsigned char data[4096]; };
struct non_declared;

typedef variant<
    volatile int, 
    const int, 
    const long, 
    declared, 
    non_declared, 
    std::string
>::types types;
  1. 我们可以在编译时检查所有内容。让我们断言类型不为空:
#include <boost/static_assert.hpp> 
#include <boost/mpl/empty.hpp> 

BOOST_STATIC_ASSERT((!boost::mpl::empty<types>::value)); 
  1. 我们还可以检查,例如,non_declared类型仍然在索引4位置:
#include <boost/mpl/at.hpp>
#include <boost/type_traits/is_same.hpp>

BOOST_STATIC_ASSERT((boost::is_same<
    non_declared, 
    boost::mpl::at_c<types, 4>::type
>::value));
  1. 并且最后一个类型仍然是std::string
#include <boost/mpl/back.hpp>

BOOST_STATIC_ASSERT((boost::is_same<
    boost::mpl::back<types>::type,
    std::string
>::value));
  1. 我们可以进行一些转换。让我们从去除常量和易失性限定符开始:
#include <boost/mpl/transform.hpp>
#include <boost/type_traits/remove_cv.hpp>

typedef boost::mpl::transform<
    types,
    boost::remove_cv<boost::mpl::_1>
>::type noncv_types;
  1. 这是我们如何去除重复类型的方法:
#include <boost/mpl/unique.hpp>

typedef boost::mpl::unique<
    noncv_types, 
    boost::is_same<boost::mpl::_1, boost::mpl::_2>
>::type unique_types;
  1. 我们可以检查向量只包含5种类型:
#include <boost/mpl/size.hpp>

BOOST_STATIC_ASSERT((boost::mpl::size<unique_types>::value == 5));
  1. 这是我们如何计算每个元素的大小:
// Without this we'll get an error:
// "use of undefined type 'non_declared'"
struct non_declared{};

#include <boost/mpl/sizeof.hpp>
typedef boost::mpl::transform<
    unique_types, 
    boost::mpl::sizeof_<boost::mpl::_1>
>::type sizes_types;
  1. 这是如何从sizes_type类型中获取最大大小的:
#include <boost/mpl/max_element.hpp>

typedef boost::mpl::max_element<sizes_types>::type max_size_type;

我们可以断言类型的最大大小等于结构声明的大小,这必须是我们示例中最大的大小:

  BOOST_STATIC_ASSERT(max_size_type::type::value == sizeof(declared)); 

它是如何工作的…

boost::mpl::vector类是一个在编译时保存类型的容器。更准确地说,它是一个保存类型的类型。我们不创建它的实例;相反,我们只是在typedef中使用它。

与标准库容器不同,Boost.MPL容器没有成员方法。相反,方法在单独的头文件中声明。因此,要使用一些方法,我们需要:

  1. 包含正确的头文件。

  2. 通常通过指定容器作为第一个参数来调用该方法。

我们已经在第四章中看到了元函数,编译时技巧。我们使用了一些元函数(如boost::is_same)来自熟悉的Boost.TypeTraits库。

因此,在步骤 3步骤 4步骤 5中,我们只是为我们的容器类型调用元函数。

最困难的部分即将到来!

占位符被Boost.MPL库广泛用于组合元函数:

typedef boost::mpl::transform<
    types,
    boost::remove_cv<boost::mpl::_1>
>::type noncv_types;

在这里,boost::mpl::_1 是一个占位符,整个表达式的意思是,对于 types 中的每种类型,执行 boost::remove_cv<>::type 并将该类型推回到结果向量中。通过 ::type 返回结果向量。

让我们继续到 步骤 7。在这里,我们使用 boost::is_same<boost::mpl::_1, boost::mpl::_2> 模板参数为 boost::mpl::unique 指定了一个比较元函数,其中 boost::mpl::_1boost::mpl::_2 是占位符。你可能会发现它类似于 boost::bind(std::equal_to(), _1, _2)步骤 7 中的整个表达式类似于以下伪代码:

std::vector<type> t; // 't' stands for 'types'. 
std::unique(t.begin(), t.end(), boost::bind(std::equal_to<type>(), _1, _2)); 

步骤 9 中有一些有趣的东西,这对于更好地理解是必要的。在前面的代码中,sizes_types 不是一个值的向量,而是一个表示数字的整数常量类型的向量。sizes_types typedef 实际上是以下类型:

struct boost::mpl::vector<
    struct boost::mpl::size_t<4>,
    struct boost::mpl::size_t<4>,
    struct boost::mpl::size_t<4096>,
    struct boost::mpl::size_t<1>,
    struct boost::mpl::size_t<32> 
>

最后一步现在一定很清楚了。它只是从 sizes_types typedef 中获取最大的元素。

我们可以在任何允许 typedef 的地方使用 Boost.MPL 元函数。

还有更多…

Boost.MPL 库的使用会导致更长的编译时间,但可以让您对类型进行任何想要的操作。它不会增加运行时开销,甚至不会向结果二进制文件添加一条指令。C++17 没有 Boost.MPL 类,而 Boost.MPL 也不使用现代 C++ 的特性,比如可变模板。这使得在 C++11 编译器上,Boost.MPL 的编译时间不会尽可能短,但使得该库可以在 C++03 编译器上使用。

另请参阅

  • 参见 第四章,编译时技巧,了解元编程的基础知识

  • 操作类型向量 配方将为您提供有关元编程和 Boost.MPL 库的更多信息

  • 查看 boost.org/libs/mpl 上的 Boost.MPL 官方文档,了解更多示例和完整参考资料

操作类型向量

这个配方的任务是根据第二个 boost::mpl::vector 函数的内容修改一个 boost::mpl::vector 函数的内容。我们将调用第二个向量为修改器向量,每个修改器可能具有以下类型:

// Make unsigned.
struct unsigne; // Not a typo: `unsigned` is a keyword, we can not use it.

// Make constant.
struct constant;

// Otherwise we do not change type.
struct no_change;

那么,我们从哪里开始呢?

准备工作

需要基本了解 Boost.MPL。阅读 使用类型向量 配方和 第四章,编译时技巧,可能会有所帮助。

如何做…

这个配方与之前的配方类似,但它还使用了条件编译时语句。准备好了,这不会容易!

  1. 我们将从头文件开始:
// We'll need this at step 3.
#include <boost/mpl/size.hpp>
#include <boost/type_traits/is_same.hpp>
#include <boost/static_assert.hpp>

// We'll need this at step 4.
#include <boost/mpl/if.hpp>
#include <boost/type_traits/make_unsigned.hpp>
#include <boost/type_traits/add_const.hpp>

// We'll need this at step 5.
#include <boost/mpl/transform.hpp>
  1. 现在,让我们将所有的元编程魔法放入结构中,以便更简单地重用:
template <class Types, class Modifiers>
struct do_modifications {
  1. 检查传递的向量是否具有相同的大小是一个好主意:
    BOOST_STATIC_ASSERT((boost::is_same<
        typename boost::mpl::size<Types>::type, 
        typename boost::mpl::size<Modifiers>::type 
    >::value));
  1. 现在,让我们处理修改元函数:
    typedef boost::mpl::if_<
        boost::is_same<boost::mpl::_2, unsigne>,
        boost::make_unsigned<boost::mpl::_1>,
        boost::mpl::if_<
            boost::is_same<boost::mpl::_2, constant>,
            boost::add_const<boost::mpl::_1>,
            boost::mpl::_1
        >
    > binary_operator_t;
  1. 最后一步:
    typedef typename boost::mpl::transform<
        Types,
        Modifiers,
        binary_operator_t
    >::type type;
};

现在,让我们运行一些测试,确保我们的元函数运行良好:

#include <boost/mpl/vector.hpp>
#include <boost/mpl/at.hpp>

typedef boost::mpl::vector<
    unsigne, no_change, constant, unsigne
> modifiers;

typedef boost::mpl::vector<
    int, char, short, long
> types;

typedef do_modifications<types, modifiers>::type result_type;

BOOST_STATIC_ASSERT((boost::is_same<
    boost::mpl::at_c<result_type, 0>::type,
    unsigned int
>::value));

BOOST_STATIC_ASSERT((boost::is_same<
    boost::mpl::at_c<result_type, 1>::type,
    char
>::value));

BOOST_STATIC_ASSERT((boost::is_same<
    boost::mpl::at_c<result_type, 2>::type,
    const short
>::value));

BOOST_STATIC_ASSERT((boost::is_same<
    boost::mpl::at_c<result_type, 3>::type,
    unsigned long
>::value));

它是如何工作的…

步骤 3 中,我们断言大小相等,但我们以一种不寻常的方式来做。boost::mpl::size<Types>::type 元函数实际上返回一个整数常量 struct boost::mpl::long_<4>,因此在静态断言中,我们实际上比较的是两种类型,而不是两个数字。这可以以更熟悉的方式重写:

    BOOST_STATIC_ASSERT((
        boost::mpl::size<Types>::type::value
        ==
        boost::mpl::size<Modifiers>::type::value
    ));

请注意我们使用的 typename 关键字。没有它,编译器无法确定 ::type 到底是一个类型还是某个变量。之前的配方不需要它,因为在使用它们的地方,元函数的参数是完全已知的。但在这个配方中,元函数的参数是一个模板。

在处理步骤 4之前,我们将先看一下步骤 5。在步骤 5中,我们将TypesModifiersbinary_operator_t参数从步骤 4传递给boost::mpl::transform元函数。这个元函数非常简单–对于每个传递的向量,它获取一个元素并将其传递给第三个参数–一个二进制元函数。如果我们用伪代码重写它,它将看起来像下面这样:

void boost_mpl_transform_pseoudo_code() {
    vector result;
    for (std::size_t i = 0; i < Types.size(); ++i) {
        result.push_back(
            binary_operator_t(Types[i], Modifiers[i])
        );
    }
    return result;
}

步骤 4可能会让某些人头疼。在这一步中,我们为TypesModifiers向量中的每对类型编写一个元函数(请参阅前面的伪代码):

    typedef boost::mpl::if_<
        boost::is_same<boost::mpl::_2, unsigne>,
        boost::make_unsigned<boost::mpl::_1>,
        boost::mpl::if_<
            boost::is_same<boost::mpl::_2, constant>,
            boost::add_const<boost::mpl::_1>,
            boost::mpl::_1
        >
    > binary_operator_t;

正如我们已经知道的,boost::mpl::_2boost::mpl::_1是占位符。在这个配方中,_1Types向量中类型的占位符,_2Modifiers向量中类型的占位符。

因此,整个元函数的工作方式如下:

  1. 将传递给它的第二个参数(通过_2)与一个unsigned类型进行比较。

  2. 如果类型相等,使传递给它的第一个参数(通过_1)变为无符号,并返回该类型。

  3. 否则,它将传递给它的第二个参数(通过_2)与一个常量类型进行比较。

  4. 如果类型相等,它会使传递给它的第一个参数(通过_1)变为常量,并返回该类型。

  5. 否则,它返回传递给它的第一个参数(通过_1)。

在构建这个元函数时,我们需要非常小心。还需要特别注意不要在最后调用::type

>::type binary_operator_t; // INCORRECT! 

如果我们调用::type,编译器将尝试在此处评估二进制运算符,这将导致编译错误。在伪代码中,这样的尝试看起来像这样:

binary_operator_t foo; 
// Attempt to call binary_operator_t::operator() without parameters, 
// when it has only two parameters overloads. 
foo(); 

还有更多…

使用元函数需要一些实践。即使是您谦卑的仆人也不能在第一次尝试时正确地编写一些函数(尽管第二次和第三次尝试也不好)。不要害怕或困惑去尝试!

Boost.MPL库不是 C++17 的一部分,也不使用现代 C++特性,但可以与 C++11 可变模板一起使用:

template <class... T> 
struct vt_example { 
    typedef typename boost::mpl::vector<T...> type; 
}; 

BOOST_STATIC_ASSERT((boost::is_same< 
    boost::mpl::at_c<vt_example<int, char, short>::type, 0>::type, 
    int 
>::value)); 

就像以往一样,元函数不会向生成的二进制文件添加一条指令,也不会使性能变差。但是,使用它们可以使您的代码更加适应特定情况。

另请参阅

  • 从头开始阅读本章,以获取Boost.MPL用法的更多简单示例

  • 参见第四章,编译时技巧,特别是为模板参数选择最佳运算符配方,其中包含类似于binary_operator_t元函数的代码

  • Boost.MPL的官方文档在boost.org/libs/mpl上有更多示例和完整的目录

在编译时获取函数的结果类型

C++11 添加了许多良好的功能,以简化元编程。其中一个功能是替代函数语法。它允许推断模板函数的结果类型。这里是一个例子:

template <class T1, class T2>
auto my_function_cpp11(const T1& v1, const T2& v2)
    -> decltype(v1 + v2)
{
    return v1 + v2;
}

它使我们更容易编写通用函数:

#include <cassert>

struct s1 {};
struct s2 {};
struct s3 {};

inline s3 operator + (const s1& /*v1*/, const s2& /*v2*/) {
    return s3();
}

inline s3 operator + (const s2& /*v1*/, const s1& /*v2*/) {
    return s3();
}

int main() {
    s1 v1;
    s2 v2;

    s3 res0 = my_function_cpp11(v1, v2);
    assert(my_function_cpp11('\0', 1) == 1);
}

但是,Boost 有很多类似的函数,它不需要 C++11 就可以工作。这是怎么可能的,我们如何制作my_function_cpp11函数的 C++03 版本?

准备工作

这个配方需要基本的 C++和模板知识。

如何做…

C++11 极大地简化了元编程。必须使用 C++03 编写大量代码,以实现接近替代函数语法的功能:

  1. 我们必须包含以下头文件:
#include <boost/type_traits/common_type.hpp>
  1. 现在,让我们为任何类型在result_of命名空间中制作一个元函数:
namespace result_of {

    template <class T1, class T2>
    struct my_function_cpp03 {
        typedef typename boost::common_type<T1, T2>::type type;
    };
  1. 并为类型s1s2专门化它:
    template <> 
    struct my_function_cpp03<s1, s2> {
        typedef s3 type;
    };

    template <>
    struct my_function_cpp03<s2, s1> {
        typedef s3 type;
    };
} // namespace result_of
  1. 现在我们准备写my_function_cpp03函数:
template <class T1, class T2>
typename result_of::my_function_cpp03<T1, T2>::type
    my_function_cpp03(const T1& v1, const T2& v2)
{
    return v1 + v2;
}

就是这样!现在,我们可以像使用 C++11 一样使用这个函数:

int main() {
    s1 v1;
    s2 v2;

    s3 res1 = my_function_cpp03(v1, v2);
    assert(my_function_cpp03('\0', 1) == 1);
}

工作原理…

这个食谱的主要思想是,我们可以制作一个特殊的元函数来推断结果类型。这样的技术可以在 Boost 库的各个地方看到,例如,在Boost.Variantboost::get<>实现中,或者在Boost.Fusion的几乎任何函数中。

现在,让我们一步一步地进行。result_of命名空间只是一种传统,但您可以使用自己的,这并不重要。boost::common_type<>元函数推断出几种类型的公共类型,因此我们将其用于一般情况。我们还为s1s2类型添加了result_of::my_function_cpp03结构的两个模板特化。

在 C++03 中编写元函数的缺点是,有时我们需要写很多代码。比较my_function_cpp11my_function_cpp03的代码量,包括result_of命名空间,以感受其中的差异。

当元函数准备好后,我们可以在没有 C++11 的情况下推断出结果类型:

template <class T1, class T2>
typename result_of::my_function_cpp03<T1, T2>::type
    my_function_cpp03(const T1& v1, const T2& v2);

还有更多…

这种技术不会增加运行时开销,但可能会稍微减慢编译速度。您也可以在现代 C++编译器上使用它。

另请参阅

  • 第四章的食谱启用整数类型的模板函数使用禁用实数类型的模板函数使用为模板参数选择最佳运算符将为您提供有关Boost.TypeTraits和元编程的更多信息

  • 考虑官方文档Boost.TypeTraits,了解有关准备好的元函数的更多信息boost.org/libs/type_traits

制作高阶元函数

接受其他函数作为输入参数或返回其他函数的函数称为高阶函数。例如,以下函数是高阶函数:

typedef void(*function_t)(int);

function_t higher_order_function1();
void higher_order_function2(function_t f);
function_t higher_order_function3(function_t f); f); 

我们已经在本章的使用类型类型向量操作类型向量食谱中看到了高阶元函数,我们在那里使用了boost::mpl::transform

在这个食谱中,我们将尝试制作自己的高阶元函数,名为coalesce,它接受两种类型和两个元函数。coalesce元函数将第一个类型参数应用于第一个元函数,并将结果类型与boost::mpl::false_类型进行比较。如果结果类型是boost::mpl::false_类型,则返回将第二个类型参数应用于第二个元函数的结果,否则返回第一个结果类型:

template <class Param1, class Param2, class Func1, class Func2>
struct coalesce;

准备好了

这个食谱(和章节)有点棘手。强烈建议从头开始阅读本章。

如何做…

Boost.MPL元函数实际上是可以轻松作为模板参数传递的结构。困难的部分是正确使用它:

  1. 我们需要以下头文件来编写高阶元函数:
#include <boost/mpl/apply.hpp>
#include <boost/mpl/if.hpp>
#include <boost/type_traits/is_same.hpp>
  1. 下一步是评估我们的函数:
template <class Param1, class Param2, class Func1, class Func2>
struct coalesce {
    typedef typename boost::mpl::apply<Func1, Param1>::type type1;
    typedef typename boost::mpl::apply<Func2, Param2>::type type2;
  1. 现在,我们需要选择正确的结果类型:
    typedef typename boost::mpl::if_<
        boost::is_same< boost::mpl::false_, type1>,
        type2,
        type1
    >::type type;
};

就是这样!我们已经完成了一个高阶元函数!现在,我们可以像这样使用它:

#include <boost/static_assert.hpp>
#include <boost/mpl/not.hpp>
#include <boost/mpl/next.hpp>

using boost::mpl::_1;
using boost::mpl::_2;

typedef coalesce<
    boost::mpl::true_,
    boost::mpl::int_<5>,
    boost::mpl::not_<_1>,
    boost::mpl::next<_1>
>::type res1_t;
BOOST_STATIC_ASSERT((res1_t::value == 6));

typedef coalesce<
    boost::mpl::false_,
    boost::mpl::int_<5>,
    boost::mpl::not_<_1>,
    boost::mpl::next<_1>
>::type res2_t;
BOOST_STATIC_ASSERT((res2_t::value));

工作原理…

编写高阶元函数的主要问题是要注意占位符。这就是为什么我们不应该直接调用Func1<Param1>::type。而是必须使用boost::mpl::apply元函数,它接受一个函数和最多五个参数,这些参数将传递给这个函数。

您可以配置boost::mpl::apply以接受更多参数,将BOOST_MPL_LIMIT_METAFUNCTION_ARITY宏定义为所需的参数数量,例如为 6。

还有更多…

C++11 没有任何接近Boost.MPL库应用元函数的东西。

现代 C++有很多功能,可以帮助你实现Boost.MPL的功能。例如,C++11 有一个<type_traits>头文件和基本 constexpr支持。C++14 有扩展 constexpr支持,C++17 有一个可以与元组一起使用并且可以在常量表达式中使用的std::apply函数。此外,在 C++17 中,lambda 默认是 constexpr,并且有一个if constexpr(expr)。

编写自己的解决方案会浪费很多时间,而且可能在旧编译器上无法工作。因此,Boost.MPL仍然是最适合元编程的解决方案之一。

另请参阅

查看官方文档,特别是Tutorial部分,了解有关Boost.MPL的更多信息,请访问boost.org/libs/mpl

惰性评估元函数

惰性评估意味着在真正需要其结果之前不会调用函数。了解这个方法对于编写良好的元函数非常重要。惰性评估的重要性将在以下示例中展示。

想象一下,我们正在编写一些元函数,它接受一个函数Func,一个参数Param和一个条件Cond。如果将Cond应用于Param返回false,那么该函数的结果类型必须是一个fallback类型,否则结果必须是将Func应用于Param的结果:

struct fallback;

template <
        class Func,
        class Param,
        class Cond,
        class Fallback = fallback>
struct apply_if; 

这个元函数是我们无法离开惰性评估的地方,因为如果Cond不满足,可能无法将Func应用于Param。这样的尝试总是会导致编译失败,并且永远不会返回Fallback

准备工作

阅读第四章,Compile-time Tricks,是非常推荐的。然而,对元编程的良好了解应该足够了。

如何做…

注意一些小细节,比如在示例中不调用::type

  1. 我们需要以下头文件:
#include <boost/mpl/apply.hpp>
#include <boost/mpl/eval_if.hpp>
#include <boost/mpl/identity.hpp>
  1. 函数的开始很简单:
template <class Func, class Param, class Cond, class Fallback>
struct apply_if {
    typedef typename boost::mpl::apply<
        Cond, Param
    >::type condition_t;
  1. 我们在这里要小心:
    typedef boost::mpl::apply<Func, Param> applied_type; 
  1. 在评估表达式时需要额外小心:
    typedef typename boost::mpl::eval_if_c<
        condition_t::value,
        applied_type,
        boost::mpl::identity<Fallback>
    >::type type;
};

就是这样!现在我们可以自由地这样使用它:

#include <boost/static_assert.hpp>
#include <boost/type_traits/is_integral.hpp>
#include <boost/type_traits/make_unsigned.hpp>
#include <boost/type_traits/is_same.hpp>

using boost::mpl::_1;
using boost::mpl::_2;

typedef apply_if<
    boost::make_unsigned<_1>,
    int,
    boost::is_integral<_1>
>::type res1_t;

BOOST_STATIC_ASSERT((
    boost::is_same<res1_t, unsigned int>::value
));

typedef apply_if<
    boost::make_unsigned<_1>,
    float,
    boost::is_integral<_1>
>::type res2_t;

BOOST_STATIC_ASSERT((
    boost::is_same<res2_t, fallback>::value
));

它是如何工作的…

这个方法的主要思想是,如果条件为false,我们就不应该执行元函数,因为当条件为false时,该类型的元函数可能无法应用:

// Will fail with static assertion somewhere deeply in the implementation
// of boost::make_unsigned<_1> if we do not evaluate the function lazily.
typedef apply_if<
    boost::make_unsigned<_1>,
    float,
    boost::is_integral<_1>
>::type res2_t;

BOOST_STATIC_ASSERT((
    boost::is_same<res2_t, fallback>::value
));

那么,我们如何才能惰性评估元函数呢?

如果没有访问元函数的内部类型或值,编译器将不会查看元函数的内部。换句话说,当我们通过::尝试获取其成员之一时,编译器会尝试编译元函数。这可以是对::type::value的调用。这就是apply_if的不正确版本的样子:

template <class Func, class Param, class Cond, class Fallback>
struct apply_if {
    typedef typename boost::mpl::apply<
        Cond, Param
    >::type condition_t;

    // Incorrect: metafunction is evaluated when `::type` called.
    typedef typename boost::mpl::apply<Func, Param>::type applied_type;

    typedef typename boost::mpl::if_c<
        condition_t::value,
        applied_type,
        boost::mpl::identity<Fallback>
    >::type type;
};

这与我们的示例不同,在步骤 3中我们没有调用::type,并且使用eval_if_c实现了步骤 4,它只对其一个参数调用::typeboost::mpl::eval_if_c元函数的实现如下:

template<bool C, typename F1, typename F2>
struct eval_if_c {
    typedef typename if_c<C,F1,F2>::type f_;
    typedef typename f_::type type; // call `::type` only for one parameter
};

因为boost::mpl::eval_if_c对于成功的条件调用了::type,而fallback没有::type,所以我们需要将fallback包装到boost::mpl::identity类中。这个类非常简单,但是通过::type调用返回其模板参数,并且不执行其他操作:

template <class T> 
struct identity { 
    typedef T type; 
}; 

还有更多…

正如我们已经提到的,C++11 没有Boost.MPL的类,但我们可以像使用boost::mpl::identity<T>一样,使用带有单个参数的std::common_type<T>

和往常一样,元函数不会在输出的二进制文件中增加一行代码,你可以随意使用元函数。在编译时做得越多,运行时剩下的就越少。

另请参阅…

  • boost::mpl::identity类型可用于禁用模板函数的Argument Dependent LookupADL)。请参阅<boost/implicit_cast.hpp>头文件中boost::implicit_cast的源代码。

  • 从头开始阅读本章和Boost.MPL的官方文档,网址为boost.org/libs/mpl,可能会有所帮助。

将所有元组元素转换为字符串

这个配方和下一个配方都致力于混合编译时和运行时特性。我们将使用Boost.Fusion库并看看它能做什么。

还记得我们在第一章谈论过元组和数组吗?现在,我们想要编写一个单一的函数,可以将元组和数组的元素流式传输到字符串。

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

准备工作

您应该了解boost::tupleboost::array类以及boost::lexical_cast函数。

如何做…

我们已经几乎了解了本配方中将要使用的所有函数和类。我们只需要把它们全部聚集在一起:

  1. 我们需要编写一个将任何类型转换为字符串的函数:
#include <boost/lexical_cast.hpp>
#include <boost/noncopyable.hpp>

struct stringize_functor: boost::noncopyable {
private:
    std::string& result;

public:
    explicit stringize_functor(std::string& res)
        : result(res)
    {}

    template <class T>
    void operator()(const T& v) const {
        result += boost::lexical_cast<std::string>(v);
    }
};
  1. 现在是代码的棘手部分:
#include <boost/fusion/include/for_each.hpp>

template <class Sequence>
std::string stringize(const Sequence& seq) {
    std::string result;
    boost::fusion::for_each(seq, stringize_functor(result));
    return result;
}

到此为止!现在,我们可以将任何想要的东西转换为字符串:

#include <iostream>
#include <boost/fusion/include/vector.hpp>
#include <boost/fusion/adapted/boost_tuple.hpp>
#include <boost/fusion/adapted/std_pair.hpp>
#include <boost/fusion/adapted/boost_array.hpp>

struct cat{};

std::ostream& operator << (std::ostream& os, const cat& ) {
    return os << "Meow! ";
}

int main() {
    boost::fusion::vector<cat, int, std::string> tup1(cat(), 0, "_0");
    boost::tuple<cat, int, std::string> tup2(cat(), 0, "_0");
    std::pair<cat, cat> cats;
    boost::array<cat, 10> many_cats;

    std::cout << stringize(tup1) << '\n' 
        << stringize(tup2) << '\n'
        << stringize(cats) << '\n'
        << stringize(many_cats) << '\n';
}

前面的例子输出如下:

 Meow! 0_0
 Meow! 0_0
 Meow! Meow! 
 Meow! Meow! Meow! Meow! Meow! Meow! Meow! Meow! Meow! Meow!

它是如何工作的…

stringize函数的主要问题是,boost::tuplestd::pair都没有begin()end()方法,所以我们无法调用std::for_each。这就是Boost.Fusion发挥作用的地方。

Boost.Fusion库包含许多可以在编译时操作结构的出色算法。

boost::fusion::for_each函数遍历序列的元素,并对每个元素应用一个函数。

请注意我们已经包括了:

#include <boost/fusion/adapted/boost_tuple.hpp> 
#include <boost/fusion/adapted/std_pair.hpp> 
#include <boost/fusion/adapted/boost_array.hpp> 

这是必需的,因为默认情况下Boost.Fusion只能使用自己的类。Boost.Fusion有自己的元组类,boost::fusion::vector,它与boost::tuple非常接近:

#include <string>
#include <cassert>

#include <boost/tuple/tuple.hpp>

#include <boost/fusion/include/vector.hpp>
#include <boost/fusion/include/at_c.hpp>

void tuple_example() {
    boost::tuple<int, int, std::string> tup(1, 2, "Meow");
    assert(boost::get<0>(tup) == 1);
    assert(boost::get<2>(tup) == "Meow");
}

void fusion_tuple_example() {
    boost::fusion::vector<int, int, std::string> tup(1, 2, "Meow");
    assert(boost::fusion::at_c<0>(tup) == 1);
    assert(boost::fusion::at_c<2>(tup) == "Meow");
}

boost::fusion::vector不像boost::tuple那么简单。我们将在拆分元组配方中看到两者之间的区别。

还有更多…

boost::fusion::for_eachstd::for_each之间有一个根本的区别。std::for_each函数内部包含一个循环,并在运行时确定必须执行多少次迭代。然而,boost::fusion::for_each()在编译时知道迭代次数,并完全展开循环。对于boost::tuple<cat, int, std::string> tup2boost::fusion::for_each(tup2, functor)调用等同于以下代码:

    functor(boost::fusion::at_c<0>(tup2));
    functor(boost::fusion::at_c<1>(tup2));
    functor(boost::fusion::at_c<2>(tup2));

C++11 不包含Boost.Fusion类。Boost.Fusion的所有方法都非常有效。它们尽可能多地在编译时执行,并具有一些非常高级的优化。

C++14 添加了std::integer_sequencestd::make_integer_sequence来简化使用可变模板的for。使用这些实体,可以手动编写boost::fusion::for_each功能,并在没有Boost.Fusion的情况下实现stringize函数:

#include <utility>
#include <tuple>

template <class Tuple, class Func, std::size_t... I>
void stringize_cpp11_impl(const Tuple& t, const Func& f, std::index_sequence<I...>) {
    // Oops. Requires C++17 fold expressions feature.
    // (f(std::get<I>(t)), ...);

    int tmp[] = { 0, (f(std::get<I>(t)), 0)... };
    (void)tmp; // Suppressing unused variable warnings.
}

template <class Tuple>
std::string stringize_cpp11(const Tuple& t) {
    std::string result;
    stringize_cpp11_impl(
        t,
        stringize_functor(result),
        std::make_index_sequence< std::tuple_size<Tuple>::value >()
    );
    return result;
}

正如你所看到的,有很多代码被编写来做到这一点,这样的代码并不容易阅读和理解。

关于在 C++20 标准中添加类似于constexpr for的功能的想法在 C++标准化工作组中进行了讨论。有了这个功能,有一天我们可以编写以下代码(语法可能会改变!):

template <class Tuple>
std::string stringize_cpp20(const Tuple& t) {
    std::string result;
    for constexpr(const auto& v: t) {
        result += boost::lexical_cast<std::string>(v);
    }
    return result;
}

在那之前,Boost.Fusion似乎是最通用和简单的解决方案。

另请参阅

  • 拆分元组配方将提供有关Boost.Fusion真正能力的更多信息。

  • Boost.Fusion的官方文档包含一些有趣的例子和完整的参考资料,可以在boost.org/libs/fusion找到

拆分元组

这个配方将展示Boost.Fusion库能力的一小部分。我们将把一个单一的元组分成两个元组,一个包含算术类型,另一个包含所有其他类型。

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

准备工作

这个配方需要了解Boost.MPL,占位符和Boost.Tuple。建议从头开始阅读本章。

如何做…

这可能是本章中最难的配方之一。生成的类型在编译时确定,并且这些类型的值在运行时填充:

  1. 为了实现这种混合,我们需要以下头文件:
#include <boost/fusion/include/remove_if.hpp>
#include <boost/type_traits/is_arithmetic.hpp>
  1. 现在,我们准备编写一个返回非算术类型的函数:
template <class Sequence>
typename boost::fusion::result_of::remove_if<
    const Sequence, 
    boost::is_arithmetic<boost::mpl::_1> 
>::type get_nonarithmetics(const Sequence& seq) 
{
    return boost::fusion::remove_if< 
        boost::is_arithmetic<boost::mpl::_1> 
    >(seq);
}
  1. 以及一个返回算术类型的函数:
template <class Sequence>
typename boost::fusion::result_of::remove_if<
    const Sequence, 
    boost::mpl::not_< boost::is_arithmetic<boost::mpl::_1> >
>::type get_arithmetics(const Sequence& seq) 
{
    return boost::fusion::remove_if< 
        boost::mpl::not_< boost::is_arithmetic<boost::mpl::_1> >
    >(seq);
}

就是这样!现在,我们能够执行以下任务:

#include <boost/fusion/include/vector.hpp>
#include <cassert>
#include <boost/fusion/include/at_c.hpp>
#include <boost/blank.hpp>

int main() {
    typedef boost::fusion::vector<
        int, boost::blank, boost::blank, float
    > tup1_t;
    tup1_t tup1(8, boost::blank(), boost::blank(), 0.0);

    boost::fusion::vector<boost::blank, boost::blank> res_na
        = get_nonarithmetics(tup1);
    boost::fusion::vector<int, float> res_a = get_arithmetics(tup1);
    assert(boost::fusion::at_c<0>(res_a) == 8);
}

工作原理…

Boost.Fusion的理念是编译器在编译时知道结构布局,无论编译器在编译时知道什么,我们都可以同时改变。Boost.Fusion允许我们修改不同的序列,添加和删除字段,并更改字段类型。这就是我们在步骤 2步骤 3中所做的;我们从元组中删除了非必需的字段。

现在,让我们仔细看看get_nonarithmetics。首先,它的结果类型是使用以下结构推导出来的:

typename boost::fusion::result_of::remove_if<
    const Sequence, 
    boost::is_arithmetic<boost::mpl::_1> 
>::type

这对我们来说应该很熟悉。我们在本章的在编译时获取函数结果类型配方中看到了类似的东西。Boost.MPL的占位符boost::mpl::_1boost::fusion::result_of::remove_if元函数很搭配,它返回一个新的序列类型。

现在,让我们进入函数内部,看看以下代码:

    return boost::fusion::remove_if< 
        boost::is_arithmetic<boost::mpl::_1> 
    >(seq);

记住编译器在编译时知道seq的所有类型。这意味着Boost.Fusion可以为seq的不同元素应用元函数,并为它们获取元函数结果。这也意味着Boost.Fusion知道如何从旧结构复制必需的字段到新结构中。

然而,Boost.Fusion尽可能地避免复制字段。

步骤 3中的代码与步骤 2中的代码非常相似,但它具有一个用于删除非必需类型的否定谓词。

我们的函数可以与Boost.Fusion支持的任何类型一起使用,而不仅仅是boost::fusion::vector

还有更多…

您可以为Boost.Fusion容器使用Boost.MPL函数。您只需要包含#include <boost/fusion/include/mpl.hpp>

#include <boost/fusion/include/mpl.hpp>
#include <boost/mpl/transform.hpp>
#include <boost/type_traits/remove_const.hpp>

template <class Sequence>
struct make_nonconst: boost::mpl::transform<
    Sequence,
    boost::remove_const<boost::mpl::_1>
> {};

typedef boost::fusion::vector<
    const int, const boost::blank, boost::blank
> type1;
typedef make_nonconst<type1>::type nc_type;

BOOST_STATIC_ASSERT((boost::is_same<
    boost::fusion::result_of::value_at_c<nc_type, 0>::type,
    int
>::value));

BOOST_STATIC_ASSERT((boost::is_same<
    boost::fusion::result_of::value_at_c<nc_type, 1>::type,
    boost::blank
>::value));

BOOST_STATIC_ASSERT((boost::is_same<
    boost::fusion::result_of::value_at_c<nc_type, 2>::type,
    boost::blank
>::value));

我们使用了boost::fusion::result_of::value_at_c而不是boost::fusion::result_of::at_c,因为boost::fusion::result_of::at_c返回boost::fusion::at_c调用的确切返回类型,即引用。boost::fusion::result_of::value_at_c返回没有引用的类型。

Boost.FusionBoost.MPL库不是 C++17 的一部分。Boost.Fusion非常快。它有许多优化。

值得一提的是,我们只看到了Boost.Fusion能力的一小部分。可以写一本单独的书来介绍它。

另请参阅

在 C++14 中操作异构容器

本章中我们看到的大多数元编程技巧都是在 C++11 之前发明的。可能你已经听说过其中的一些东西。

怎么样来点全新的?怎么样用 C++14 实现上一个配方,使用一个将元编程颠倒过来并让你眉毛竖起来的库?系好安全带,我们要进入Boost.Hana的世界了。

准备工作

这个配方需要了解 C++11 和 C++14,特别是 lambda 表达式。您需要一个真正兼容 C++14 的编译器来编译示例。

如何做…

现在,让我们用Boost.Hana的方式来做一切:

  1. 从包含头文件开始:
#include <boost/hana/traits.hpp>
  1. 我们创建了一个is_arithmetic_函数对象:
constexpr auto is_arithmetic_ = [](const auto& v) {
    auto type = boost::hana::typeid_(v);
    return boost::hana::traits::is_arithmetic(type);
};
  1. 现在,我们实现get_nonarithmetics函数:
#include <boost/hana/remove_if.hpp>

template <class Sequence>
auto get_nonarithmetics(const Sequence& seq)  {
    return boost::hana::remove_if(seq, [](const auto& v) {
        return is_arithmetic_(v);
    });
}
  1. 让我们用另一种方式定义get_arithmetics。就是为了好玩!
#include <boost/hana/filter.hpp>

constexpr auto get_arithmetics = [](const auto& seq) {
    return boost::hana::filter(seq, is_arithmetic_);
};

就是这样。现在,我们可以使用这些函数:

#include <boost/hana/tuple.hpp>
#include <boost/hana/integral_constant.hpp>
#include <boost/hana/equal.hpp>
#include <cassert>

struct foo {
    bool operator==(const foo&) const { return true; }
    bool operator!=(const foo&) const { return false; }
};

int main() {
    const auto tup1
        = boost::hana::make_tuple(8, foo{}, foo{}, 0.0);

    const auto res_na = get_nonarithmetics(tup1);
    const auto res_a = get_arithmetics(tup1);

    using boost::hana::literals::operator ""_c;
    assert(res_a[0_c] == 8);

    const auto res_na_expected = boost::hana::make_tuple(foo(), foo());
    assert(res_na == res_na_expected);
}

工作原理…

乍一看,代码可能看起来很简单,但事实并非如此。Boost.Hana将元编程颠倒过来了!在以前的配方中,我们直接使用类型,但Boost.Hana创建了一个保存类型并大部分时间使用变量的变量。

看一下步骤 2中的typeid_调用:

auto type = boost::hana::typeid_(v);

它实际上返回一个变量。有关类型的信息现在隐藏在type变量内部,并且可以通过调用decltype(type)::type来提取。

但让我们一行一行地来。在步骤 2中,我们将通用 lambda 存储在is_arithmetic_变量中。从这一点开始,我们可以将该变量用作函数对象。在 lambda 内部,我们创建了一个type变量,它现在保存了有关v类型的信息。下一行是对std::is_arithmetic的特殊包装,它从type变量中提取有关v类型的信息,并将其传递给std::is_arithmetic特性。该调用的结果是一个布尔整数常量。

现在,神奇的部分来了!存储在is_arithmetic_变量内的 lambda 实际上从未被boost::hana::remove_ifboost::hana::filter函数调用。所有使用它的Boost.Hana函数只需要 lambda 函数的结果类型,而不需要它的主体。我们可以安全地更改定义,整个示例将继续正常工作:

constexpr auto is_arithmetic_ = [] (const auto& v) {
    assert(false);
    auto type = boost::hana::typeid_(v);
    return boost::hana::traits::is_arithmetic(type);
};

步骤 34中,我们分别调用boost::hana::remove_ifboost::hana::filter函数。在步骤 3中,我们在 lambda 内部使用了is_arithmetic_。在步骤 4中,我们直接使用了它。你可以使用任何你喜欢的语法,这只是一个习惯问题。

最后在main()中,我们检查一切是否按预期工作,并且元组中索引为 0 的元素是否等于8

    using boost::hana::literals::operator ""_c;
    assert(res_a[0_c] == 8);

理解Boost.Hana库的最佳方法是进行实验。你可以在apolukhin.github.io/Boost-Cookbook/上在线进行。

还有更多…

还有一个小细节没有描述。operator[]如何访问元组?不可能有一个单一的函数返回不同的类型!

如果你第一次遇到这个技巧,这是非常有趣的。Boost.Hanaoperator ""_c可以与文字一起工作,并根据文字构造不同的类型:

  • 如果你写0_c,那么将返回integral_constant<long long, 0>

  • 如果你写1_c,那么将返回integral_constant<long long, 1>

  • 如果你写2_c,那么将返回integral_constant<long long, 2>

boost::hana::tuple类实际上有许多operator[]重载,接受不同类型的integral_constant。根据整数常量的值,返回正确的元组元素。例如,如果你写some_tuple[1_c],那么将调用tuple::operator[](integral_constant<long long, 1>),并返回索引为1的元素。

Boost.Hana不是 C++17 的一部分。然而,该库的作者参与了 C++标准化会议,并提出了不同的有趣事物,以纳入 C++标准。

如果你期望从Boost.Hana获得比从Boost.MPL更好的编译时间,那就不要指望了。目前编译器对Boost.Hana的方法处理得并不是非常好。也许有一天会改变。

值得一看Boost.Hana库的源代码,以发现使用 C++14 特性的新有趣方法。所有 Boost 库都可以在 GitHub 上找到github.com/boostorg

另请参阅

官方文档中有更多示例,完整的参考部分,一些更多的教程,以及一个编译时性能部分。在boost.org/libs/hana上享受Boost.Hana库。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值