C++ コア ガイドライン:文字列のルール

C++ コア ガイドラインでは、文字列という用語を一連の文字として使用しています。したがって、ガイドラインは C 文字列、C++ 文字列、C++17 の std::string_view、および std::byte に関するものです。

この投稿では、ガイドラインを大まかに参照するだけで、gsl::string_span、zstring、czstring などのガイドライン サポート ライブラリの一部である文字列は無視します。簡単に言うと、この投稿では std::string を C++ 文字列と呼び、const char* を C 文字列と呼びます。

最初のルールから始めましょう:

SL.str.1:std::string を使用 文字シーケンスを所有する

おそらく、文字シーケンスを所有する別の文字列、C-文字列を知っているでしょう。 Cストリングを使用しないでください!なんで?メモリ管理、文字列終了文字、および文字列の長さに注意する必要があるためです。

// stringC.c

#include <stdio.h>
#include <string.h>
 
int main( void ){
 
 char text[10];
 
 strcpy(text, "The Text is too long for text."); // (1) text is too big
 printf("strlen(text): %u\n", strlen(text)); // (2) text has no termination character '\0'
 printf("%s\n", text);
 
 text[sizeof(text)-1] = '\0';
 printf("strlen(text): %u\n", strlen(text));
 
 return 0;
}

単純なプログラム stringC.c には、インライン (1) とライン (2) の未定義の動作があります。さびた GCC 4.8 でコンパイルするとうまくいくようです。

C++ バリアントには同じ問題はありません。

// stringCpp.cpp

#include <iostream>
#include <string>

int main(){
 
 std::string text{"The Text is not too long."}; 
 
 std::cout << "text.size(): " << text.size() << std::endl;
 std::cout << text << std::endl;
 
 text +=" And can still grow!";
 
 std::cout << "text.size(): " << text.size() << std::endl;
 std::cout << text << std::endl;
 
}

プログラムの出力に驚かないでください。

C++ 文字列の場合、C++ ランタイムがメモリ管理と終了文字を処理するため、エラーを起こすことはできません。さらに、インデックス演算子の代わりに at 演算子を使用して C++ 文字列の要素にアクセスする場合、境界エラーは発生しません。 at 演算子の詳細については、私の以前の投稿「C++ コア ガイドライン:境界エラーの回避」を参照してください。

C++11 を含め、C++ のどこがおかしいのでしょうか? C 文字列なしで C++ 文字列を作成する方法はありませんでした。 C ストリングを取り除きたいので、これは奇妙です。この矛盾は C++14 ではなくなりました。

SL.str.12:s 標準ライブラリ string であることを意味する文字列リテラルのサフィックス

C++14 では、C++ 文字列リテラルを取得しました。これは、サフィックス s:"cStringLiteral"s を持つ C 文字列リテラルです。

私の主張を裏付ける例をお見せしましょう:C 文字列リテラルと C++ 文字列リテラルは異なります。

// stringLiteral.cpp

#include <iostream>
#include <string>
#include <utility>

int main(){
 
 using namespace std::string_literals; // (1)

 std::string hello = "hello"; // (2)
 
 auto firstPair = std::make_pair(hello, 5);
 auto secondPair = std::make_pair("hello", 15); // (3)
 // auto secondPair = std::make_pair("hello"s, 15); // (4)
 
 if (firstPair < secondPair) std::cout << "true" << std::endl; // (5)
 
}

それは残念だ; C++ 文字列リテラルを使用するには、行 (1) に名前空間 std::string_literals を含める必要があります。この例では、行 (2) が重要な行です。 C 文字列リテラルの "hello" を使用して C++ 文字列を作成します。これが、firstPair の型が (std::string, int) であるのに対し、secondPair の型が (const char*, int) である理由です。最後に、異なる型を比較できないため、(5) 行の比較は失敗します。エラー メッセージの最後の行をよく見てください:

行 (3) の C-string-literal の代わりに行 (4) の C++-string-literal を使用すると、プログラムは期待どおりに動作します。

C++-string-literals は C++14 の機能でした。さらに3年跳びましょう。 C++17 では、std::string_view と std::byte を取得しました。特に std::string_view については既に書きました。したがって、最も重要な事実のみを要約します。

SL.str.2:std::string_view または gsl::string_span 文字シーケンスを参照する

さて、 std::string ビューは文字シーケンスのみを参照します。より明確に言うと、 std::string_view は文字シーケンスを所有していません。一連の文字のビューを表します。この一連の文字は、C++ 文字列または C 文字列にすることができます。 std::string_view には、文字シーケンスへのポインターとその長さの 2 つの情報のみが必要です。 std::string のインターフェースの読み取り部分をサポートします。 std::string に加えて、std::string_view には remove_prefix と remove_suffix の 2 つの変更操作があります。

なぜ std::string_view が必要なのでしょうか? std::string_view は非常に安価にコピーでき、メモリも必要ありません。私の以前の投稿 C++17 - std::string_view によるコピーの回避 は、std::string_view の印象的なパフォーマンス数値を示しています。

すでに述べたように、C++17 でも std::byte を取得しました。

SL.str.4:char* を使用 単一の文字と SL.str.5 を参照するには:std::byte を使用します 必ずしも文字を表すとは限らないバイト値を参照する

ルール str.4 に従わず、const char* を C 文字列として使用すると、次のような重大な問題で終わる可能性があります。

char arr[] = {'a', 'b', 'c'};

void print(const char* p)
{
 cout << p << '\n';
}

void use()
{
 print(arr); // run-time error; potentially very bad
}

関数 print の引数として使用すると、arr はポインターに減衰します。未定義の動作は、arr がゼロで終了していないことです。文字として std::byte を使用できるという印象を持っている場合は、間違っています。

std::byte は、C++ 言語定義で指定されているバイトの概念を実装する特殊な型です。つまり、バイトは整数でも文字でもないため、プログラマ エラーの影響を受けません。その仕事は、オブジェクト ストレージにアクセスすることです。その結果、そのインターフェイスはビットごとの論理演算のメソッドのみで構成されます。

namespace std { 

 template <class IntType> 
 constexpr byte operator<<(byte b, IntType shift); 
 template <class IntType> 
 constexpr byte operator>>(byte b, IntType shift); 
 constexpr byte operator|(byte l, byte r); 
 constexpr byte operator&(byte l, byte r); 
 constexpr byte operator~(byte b); 
 constexpr byte operator^(byte l, byte r); 

} 

関数 std::to_integer(std::byte b) を使用して std::byte を整数型に変換し、 std::byte{integer} を呼び出して逆に変換できます。 integer は、std::numeric_limits::max() より小さい負でない値でなければなりません。

次は?

標準ライブラリのルールについてはほぼ完了です。 iostream と C 標準ライブラリに対するいくつかの規則のみが残されています。というわけで、次の投稿で何を書きますか。


No