【現代C++】性能制御のツールボックスのstring_view


本稿では,string_viewに導入された背景から,その関連知識点と使用方法を順に紹介し,次によく見られる使用トラップについて説明し,最後にこのタイプについてまとめた.
一、背景
日常のC/C++プログラミングでは、関数にデータを渡すなど、データの伝達操作をよく行います.データの消費メモリが大きい場合、データのコピーを減らすことで、プログラムのパフォーマンスを効果的に向上させることができます.Cではポインタがこの目的を達成する標準的なデータ構造であり、C++はより安全性の高い参照タイプを導入している.したがって、C++で伝達されたデータが読み取り専用であれば、const string&はC++の天然の方法となる.しかし、これは完璧ではありません.実践から見ると、少なくとも以下のいくつかの問題があります.
  • 文字列の文字面値、文字配列、文字列ポインタの伝達は依然としてデータコピーの3種類の低級データ型がstringタイプと異なり、入力時にコンパイラは暗黙的な変換を行う必要があります.すなわち、これらのデータをコピーしてstring一時オブジェクトを生成する必要があります.const string&が指すのは、実際にはこの一時的なオブジェクトです.通常、文字列の文字面値は小さく、性能損失は無視できます.ただし、文字列ポインタや文字配列が大きい場合(ファイルの内容を読み取るなど)、頻繁なメモリ割り当てやデータコピーが発生し、プログラムのパフォーマンスに深刻な影響を及ぼす場合があります.
  • substr O(n)複雑度これは特によく使われる関数であり、std::stringにこの関数が提供されているのが幸いであり、米国では不足しているのは、毎回新しく生成されたサブストリングを返し、性能のホットスポットを引き起こしやすいことである.実は私たちの本意は元の文字列を変えるわけではありませんが、どうして元の文字列に基づいて返さないのですか.

  • C++17にはstring_viewが導入されており,以上の2つの問題をうまく解決できる.
    二、std::string_view
    名前から、データベース・ビューをクラス比することができます.viewは、このタイプがデータに記憶領域を割り当てることがなく、読み取りにのみ使用できることを示します.このデータ型は、{ , }の2つの要素によって表すことができ、実際には、このデータ型のインスタンスは、元のデータを具体的に記憶することはなく、指向するデータの開始ポインタと長さだけを記憶するので、このオーバーヘッドは非常に小さい.
    文字列ビューを使用するには、を導入する必要があります.次に、このデータ型の主なAPIについて説明します.これらのAPIは基本的にconstexprで修飾されているので、コンパイル時に文字列の文字面値をうまく処理することができ、プログラム効率を向上させることができる.
    2.1コンストラクタ
    constexpr string_view() noexcept;
    constexpr string_view(const string_view& other) noexcept = default;
    constexpr string_view(const CharT* s, size_type count);
    constexpr string_view(const CharT* s);

    基本的には自己解釈であり、唯一説明しなければならないのは、なぜ私たちのコードstring_view foo(string("abc"))がコンパイルできるのか、なぜ対応する構造関数がないのかということです.
    実際には、stringクラスがstringからstring_viewの変換オペレータを再ロードしているためです.
    したがって、operator std::basic_string_view() const noexcept;は実際に2つのステップを実行しました.
  • string_view foo(string("abc"))string("abc")のオブジェクトa
  • に変換する.
  • string_view使用対象本編string_viewから導入した背景,
  • .
    2.2カスタム文字数
    カスタム字面量もC++17に追加された特性で、定数の読みやすさが向上しました.次のコードはcppreferenceの値をとり,カスタムフォント値と文字列の意味の違いをよく説明できる.
    #include 
    #include 
     
    int main()
    {
        using namespace std::literals;
     
        std::string_view s1 = "abc\0\0def";
        std::string_view s2 = "abc\0\0def"sv;
        std::cout << "s1: " << s1.size() << " \"" << s1 << "\"
    "; std::cout << "s2: " << s2.size() << " \"" << s2 << "\"
    "; }

    出力:
    s1: 3 "abc"
    s2: 8 "abc^@^@def"

    以上の例は両者の意味の違いをよく見ることができ、string_viewは文字列にとって、文字列の終わりを表す特殊な意味があり、文字列ビューはcareではなく、実際の文字の個数に関心を持っている.
    2.3メンバー関数
    次に、そのメンバー関数を列挙します.関数の戻り値は無視され、関数にリロードがある場合は、カッコ内に\0で埋め込まれます.これにより、全体的な輪郭を持つことができます.
    //    
    begin()
    end()
    cbegin()
    cend()
    rbegin()
    rend()
    crbegin()
    crend()
     
    //   
    size()
    length()
    max_size()
    empty()
     
    //     
    operator[](size_type pos)
    at(size_type pos)
    front()
    back()
    data()
     
    //    
    remove_prefix(size_type n)
    remove_suffix(size_type n)
    swap(basic_string_view& s)
     
    copy(charT* s, size_type n, size_type pos = 0)
    string_view substr(size_type pos = 0, size_type n = npos)
    compare(...)
    starts_with(...)
    ends_with(...)
    find(...)
    rfind(...)
    find_first_of(...)
    find_last_of(...) 
    find_first_not_of(...)
    find_last_not_of(...)

    関数リストから見ると、...の読み取り専用関数とほぼ一致し、stringを使用する方法でstring_viewとほぼ一致する.いくつかの点で特に説明する必要があります.
  • stringstring_view関数の時間的複雑さはO(1)であり,背景部分の第2の問題を解決した.
  • モディファイヤの3つの関数は、substrのデータポインタのみを変更し、ポインタのデータは変更しません.

  • それ以外に、関数名は基本的に自己解釈されます.
    2.4例
    Haskellには共通の関数string_viewがあり、文字列を行に切断してコンテナに格納します.C++で実現します
    string-バージョン
    #include 
    #include 
    #include 
    #include 
    #include 
    
    void lines(std::vector<:string> &lines, const std::string &str) {
        auto sep{"
    "}; size_t start{str.find_first_not_of(sep)}; size_t end{}; while (start != std::string::npos) { end = str.find_first_of(sep, start + 1); if (end == std::string::npos) end = str.length(); lines.push_back(str.substr(start, end - start)); start = str.find_first_not_of(sep, end + 1); } }
    linesタイプで分割する文字列を受信しましたが、大きなメモリを指す文字ポインタを入力すると、プログラムの効率に影響します.const std::string &を使用すると、string_view-バージョン
    #include 
    #include 
    #include 
    #include 
    #include 
    #include 
    
    void lines(std::vector<:string> &lines, std::string_view str) {
        auto sep{"
    "}; size_t start{str.find_first_not_of(sep)}; size_t end{}; while (start != std::string_view::npos) { end = str.find_first_of(sep, start + 1); if (end == std::string_view::npos) end = str.length(); lines.push_back(std::string{str.substr(start, end - start)}); start = str.find_first_not_of(sep, end + 1); } }

    上記の例では、std::string_viewタイプをstringに変更するだけで性能の向上が得られた.一般に、プログラム内のstring_viewstringに置き換えるプロセスは、両者のメンバー関数の類似性のおかげで比較的直感的である.しかし、すべての「翻訳」プロセスがそうであるわけではありません.例えば、
    void lines(std::vector<:string> &lines, const std::string& str) {
        std::stringstream ss(str);
        std::string line;
    
        while (std::getline(ss, line, '
    ')) { lines.push_back(line); } }

    このバージョンでは、string_viewを使用してstringstream関数を実装します.linesは、対応する構造関数がstringstreamタイプのパラメータを受信していないため、直接置換することはできないので、翻訳プロセスは複雑である.
    三、落とし穴の使用
    世の中には無料の昼食はありません.string_viewの不適切な使用も一連の問題をもたらす.
  • string_viewの範囲内の文字は、string_view
  • を含まない場合がある.
    のように
    #include 
    #include 
    
    int main() {
        std::string_view str{"abc", 1};
    
        std::cout << str.data() << std::endl;
    
        return 0;
    }
    \0を印刷するつもりでしたが、aが出力されました.これは、文字列に関連する関数には、互換性Cの約束があるためです.abcは文字列の末尾を表します.上記のプログラムは、開始から文字列終了までのすべての文字を印刷し、\0に含まれる有効文字はstrであるが、acoutを認識する.このメモリ空間には合法的な文字列末尾文字があり、\0strのない文字配列を指している場合、プログラムにメモリの問題が発生する可能性が高いので、\0タイプのデータを受信文字列の関数に転送するときは注意しなければなりません.
    2.string_viewから[const] char*オブジェクトを構築する時間複雑度string_viewこれは、文字列の長さを取得するために最初から巡回する必要があるためである.O(n)のタイプに対していくつかの[const] char*の動作のみである場合、O(1)を直接使用するよりも、[const] char*に移行するのは性能上の利点がない.string_viewconst string&に比べてコピーの損失が少なくなったにすぎない.実際にはstring_viewですべての文字列を受信することができますが、このタイプはあまりにも下位で、使用しにくいです.場合によっては、[const] char*に移行するのは、string_viewのようないくつかの関数を使用したいだけかもしれません.
    3.substrが指すコンテンツのライフサイクルは、それ自体よりも短い可能性があります.string_viewは、コンテンツを指す所有権を持っていません.Rustの用語では、一時的なstring_view(借用)にすぎません.所有者が事前に解放された場合、これらのコンテンツを使用している場合は、borrow(dangling pointer)やサスペンションリファレンス(dangling references)に似たメモリの問題が発生します.Rustは、コンパイル時に変数のライフサイクルを分析するメカニズムを備えており、 のリソースが使用中に解放されないことを保証していますが、C++はこのような検査がなく、人工的な保証が必要です.以下に、典型的な問題点を示します.
    std::string_view sv = std::string{"hello world"}; 
    string_view foo() {
        std::string s{"hello world"};
        return string_view{s};
    }
    auto id(std::string_view sv) { return sv; }
    
    int main() {
        std::string s = "hello";
        auto sv = id(s + " world"); 
    }

    四、まとめborrowはいくつかの痛みを解決したが、ポインタと引用のいくつかの古い問題も導入した.C++標準はこのタイプにあまり制約を与えていません.これは、通常の変数のように多くの方法で使用することができます.例えば、パラメータを伝達することができ、関数として値を返すことができ、一般的な変数を作ることができ、容器に入れることができます.シーンの使用が複雑になるにつれて、人工的に指向されるコンテンツのライフサイクルが十分に長くなることは保証されません.したがって、推奨される使用方法は、関数パラメータとしてのみ使用されるため、このパラメータが関数内でのみ使用され、伝達されない場合は安全である.
    私の公衆番号に注目してくださいね.