# 通过集成std::char\_traits创建自定义字符串类

我们知道`std::string`非常好用。不过，对于一些朋友来说他们需要对自己定义的字符串类型进行处理。

使用它们自己的字符串类型显然不是一个好主意，因为对于字符串的安全处理是很困难的。幸运的是，`std::string`是`std::basic_string`类型的一个特化版本。这个类中包含了所有复杂的内存处理，不过其对字符串的拷贝和比较没有添加任何条件。所以我们可以基于`basic_string`，将其所需要包含的自定义类作为一个模板参数传入。

本节中，我们将来看下如何传入自定义类型。然后，在不实现任何东西的情况下，如何对自定义字符串进行创建。

## How to do it...

我们将实现两个自定义字符串类：`lc_string`和`ci_string`。第一个类将通过输入创建一个全是小写字母的字符串。另一个字符串类型不会对输入进行任何变化，不过其会对字符串进行大小写不敏感的比较：

1. 包含必要的头文件，并声明所使用的命名空间：

   ```cpp
   #include <iostream>
   #include <algorithm>
   #include <string>

   using namespace std;
   ```
2. 然后要对`std::tolower`函数进行实现，其已经定义在头文件`<cctype>`中。其函数也是现成的，不过其不是`constexpr`类型。C++17中一些`string`函数可以声明成`constexpr`类型，但是还要使用自定义的类型。所以对于输入字符串，只将大写字母转换为小写，而其他字符则不进行修改：

   ```cpp
   static constexpr char tolow(char c) {
       switch (c) {
       case 'A'...'Z': return c - 'A' + 'a'; // 读者自行将case展开
       default:         return c;
       }
   }
   ```
3. `std::basic_string`类可以接受三个模板参数：字符类型、字符特化类和分配器类型。本节中我们只会修改字符特化类，因为其定义了字符串的行为。为了重新实现与普通字符串不同的部分，我们会以`public`方式继承标准字符特化类：

   ```cpp
   class lc_traits : public char_traits<char> {
   public:
   ```
4. 我们类能接受输入字符串，并将其转化成小写字母。这里有一个函数，其是字符级别的，所以我们可以对其使用`tolow`函数。我们的这个函数为`constexpr`：

   ```cpp
       static constexpr
       void assign(char_type& r, const char_type& a ) {
           r = tolow(a);
       }
   ```
5. 另一个函数将整个字符串拷贝到我们的缓冲区内。使用`std::transform`将所有字符从源字符串中拷贝到内部的目标字符串中，同时将每个字符与其小写版本进行映射：

   ```cpp
       static char_type* copy(char_type* dest,
                               const char_type* src,
                            size_t count) {
           transform(src, src + count, dest, tolow);
           return dest;
       }
   };
   ```
6. 上面的特化类可以帮助我们创建一个字符串类，其能有效的将字符串转换成小写。接下来我们在实现一个类，其不会对原始字符串进行修改，但是其能对字符串做大小写敏感的比较。其继承于标准字符串特征类，这次将对一些函数进行重新实现：

   ```cpp
   class ci_traits : public char_traits<char> {
   public:
   ```
7. `eq`函数会告诉我们两个字符是否相等。我们也会实现一个这样的函数，但是我们只实现小写字母的版本。这样'A'与'a'就是相等的：

   ```cpp
       static constexpr bool eq(char_type a, char_type b) {
           return tolow(a) == tolow(b);
       }
   ```
8. `lt`函数会告诉我们两个字符在字母表中的大小情况。这里使用了逻辑操作符，并继续对两个字符使用转换成小写的函数：

   ```cpp
       static constexpr bool lt(char_type a, char_type b) {
           return tolow(a) < tolow(b);
       }
   ```
9. 最后两个函数都是字符级别的函数，接下来两个函数都为字符串级别的函数。`compare`函数与`strncmp`函数差不多。当两个字符串的长度`count`相等，那么就返回0。如果不相等，会返回一个负数或一个正数，返回值就代表了其中哪一个在字母表中更小。并计算两个字符串中所有字符之间的距离，当然这些都是在小写情况下进行的操作。C++14后，这个函数可以声明成`constexpr`类型：

   ```cpp
        static constexpr int compare(const char_type* s1,
                                  const char_type* s2,
                                  size_t count) {
           for (; count; ++s1, ++s2, --count) {
               const char_type diff (tolow(*s1) - tolow(*s2));
               if (diff < 0) { return -1; }
               else if (diff > 0) { return +1; }
           }
           return 0;
       }
   ```
10. 我们所需要实现的最后一个函数就是大小写不敏感的`find`函数。对于给定输入字符串`p`，其长度为`count`，我们会对某个字符`ch`的位置进行查找。然后，其会返回一个指向第一个匹配字符位置的指针，如果没有找到则返回`nullptr`。这个函数比较过程中我们需要使用`tolow`函数将字符串转换成小写，以匹配大小写不敏感的查找。不幸的是，我们不能使用`std::find_if`来做这件事，因为其是非`constexpr`函数，所以我们需要自己去写一个循环：

    ```cpp
        static constexpr
        const char_type* find(const char_type* p,
                              size_t count,
                              const char_type& ch) {
        const char_type find_c {tolow(ch)};

        for (; count != 0; --count, ++p) {
            if (find_c == tolow(*p)) { return p; }
        }
            return nullptr;
        }
    };
    ```
11. OK，所有自定义类都完成了。这里我们可以定义两种新字符串类的类型。`lc_string`代表小写字符串，`ci_string`代表大小写不敏感字符串。这两种类型与`std::string`都有所不同：

    ```cpp
    using lc_string = basic_string<char, lc_traits>;
    using ci_string = basic_string<char, ci_traits>;
    ```
12. 为了能让输出流接受新类，我们需要对输出流操作符进行重载：

    ```cpp
    ostream& operator<<(ostream& os, const lc_string& str) {
        return os.write(str.data(), str.size());
    }

    ostream& operator<<(ostream& os, const ci_string& str) {
        return os.write(str.data(), str.size());
    }
    ```
13. 现在我们来对主函数进行编写。先让我们创建一个普通字符串、小写字符串和大小写不敏感字符串的实例，然后直接将其进行打印。其在终端上看起来都很正常，不过小写字符串将所有字符转换成了小写：

    ```cpp
    int main()
    {
        cout << " string: "
            << string{"Foo Bar Baz"} << '\n'
            << "lc_string: "
            << lc_string{"Foo Bar Baz"} << '\n'
            << "ci_string: "
            << ci_string{"Foo Bar Baz"} << '\n';
    ```
14. 为了测试大小写不敏感字符串，可以实例化两个字符串，这两个字符串只有在大小写方面有所不同。当我们将这两个字符串进行比较时，其应该是相等的：

    ```cpp
        ci_string user_input {"MaGiC PaSsWoRd!"};
        ci_string password {"magic password!"};
    ```
15. 之后，对其进行比较，然后将匹配的结果进行打印：

    ```cpp
        if (user_input == password) {
            cout << "Passwords match: \"" << user_input
                 << "\" == \"" << password << "\"\n";
        }
    }
    ```
16. 编译并运行程序，其输出和我们期望的相符。开始的三行并未对输入进行修改，除了`lc_string`将所有字符转换成了小写。最后的比较，在大小写不敏感的前提下，也是相等的：

    ```cpp
    $ ./custom_string
    string: Foo Bar Baz
    lc_string: foo bar baz
    ci_string: Foo Bar Baz
    Passwords match: "MaGiC PaSsWoRd!" == "magic password!"
    ```

## How it works...

我们完成的所有子类和函数实现，在新手看来十分的不可思议。这些函数签名都来自于哪里？为什么我们为了函数签名，就要对相关功能性的函数进行重新实现呢？

首先，来看一下`std::string`的类声明：

```cpp
template <
    class CharT,
    class Traits = std::char_traits<CharT>,
    class Allocator = std::allocator<CharT>
    >
class basic_string;
```

可以看出`std::string`其实就是一个`std::basic_string<char>` 类，并且其可以扩展为`std::basic_string<char, std::char_traits<char>, std::allocator<char>>`。OK，这是一个非常长的类型描述，不过其意义何在呢？这就表示字符串可以不限于有符号类型`char`，也可以是其他类型。其对于字符串类型都是有效的，这样就不限于处理ASCII字符集。当然，这不是我们的关注点。

`char_traits<char>`类包含`basic_string`所需要的算法。`char_traits<char>`可以进行字符串间的比较、查找和拷贝。

`allocator<char>`类也是一个特化类，不过其运行时给字符串进行空间的分配和回收。这对于现在的我们来说并不重要，我们只使用其默认的方式就好。

当我们想要一个不同的字符串类型是，可以尝试对`basic_string`和`char_traits`类中提供的方法进行复用。我们实现了两个`char_traits`子类：`case_insentitive`和`lower_caser`类。我们可以将这两个字符串类替换标准`char_traits`类型。

> Note：
>
> 为了探寻`basic_string`适配的可能性，我们需要查询C++ STL文档中关于`std::char_traits`的章节，然后去了解还有那些函数需要重新实现。


---

# Agent Instructions: Querying This Documentation

If you need additional information that is not directly available in this page, you can query the documentation dynamically by asking a question.

Perform an HTTP GET request on the current page URL with the `ask` query parameter:

```
GET https://zoupers.gitbook.io/cpp-17-stl-cookbook/chapter7-0-chinese/chapter7-11-chinese.md?ask=<question>
```

The question should be specific, self-contained, and written in natural language.
The response will contain a direct answer to the question and relevant excerpts and sources from the documentation.

Use this mechanism when the answer is not explicitly present in the current page, you need clarification or additional context, or you want to retrieve related documentation sections.
