web-dev-qa-db-ja.com

char *とstd :: uint8_t *間のreinterpret_cast-安全ですか?

現在、私たちは皆、バイナリデータを処理する必要があります。 C++では、バイトのシーケンスを処理します。最初からcharがビルディングブロックでした。 sizeofが1になるように定義されています。これはバイトです。また、すべてのライブラリI/O関数は、デフォルトでcharを使用します。すべてが良いですが、常に少し懸念があり、一部の人々を悩ませた少し奇妙なことがありました-バイトのビット数は実装定義です。

したがって、C99では、固定幅の整数型である開発者が自分自身を簡単に表現できるように、いくつかのtypedefを導入することが決定されました。もちろん、オプションです。移植性を損なうことは決してないからです。その中で、uint8_tは、固定幅の8ビット符号なし整数型であるstd::uint8_tとしてC++ 11に移行され、8ビットバイトを実際に使用したい人にとって最適な選択肢でした。

そのため、開発者は新しいツールを採用し、std::uint8_t*std::vector<std::uint8_t>などの8ビットバイトシーケンスを受け入れることを明示的に示すライブラリの構築を開始しました。

しかし、おそらく非常に深い考えで、標準化委員会はstd::char_traits<std::uint8_t>の実装を必要としないことを決定したため、開発者がstd::basic_fstream<std::uint8_t>を簡単かつ移植可能にインスタンス化し、std::uint8_tsをバイナリとして簡単に読み取ることを禁止しましたデータ。あるいは、バイトのビット数を気にせず、満足している人もいます。

しかし、残念ながら、2つの世界が衝突し、場合によっては、データをchar*として取得し、std::uint8_t*を期待するライブラリに渡す必要があります。しかし、ちょっと待ってください、char変数ビットではなく、std::uint8_tは8に固定されていますか?データが失われますか?

まあ、これには興味深いStandardeseがあります。正確に1バイトを保持するように定義されたcharは、メモリのアドレス可能な最小チャンクであるため、ビット幅がcharよりも小さい型は存在できません。次に、UTF-8コード単位を保持できるように定義されています。これにより、最小-8ビットが得られます。これで、8ビット幅であることが必要なtypedefと少なくとも8ビット幅のタイプができました。しかし、代替案はありますか?はい、unsigned charcharの符号は実装によって定義されることに注意してください。他のタイプ?ありがたいことに、違います。他のすべての整数型には、8ビットの範囲外の必須範囲があります。

最後に、std::uint8_tはオプションです。つまり、このタイプを使用するライブラリは、定義されていないとコンパイルされません。しかし、それがコンパイルされたらどうでしょうか?これは、8ビットのバイトとCHAR_BIT == 8を備えたプラットフォーム上にいることを意味します。

8ビットのバイトがあり、std::uint8_tcharまたはunsigned charとして実装されていることがわかったら、reinterpret_castchar*からstd::uint8_t*へ、またはその逆?ポータブルですか?

これが私のスタンダードリーディングスキルの失敗です。安全に派生したポインタ([basic.stc.dynamic.safety])について読み、理解している限りでは、次のとおりです。

std::uint8_t* buffer = /* ... */ ;
char* buffer2 = reinterpret_cast<char*>(buffer);
std::uint8_t buffer3 = reinterpret_cast<std::uint8_t*>(buffer2);

buffer2に触れなければ安全です。私が間違っていたら訂正してください。

したがって、次の前提条件を前提とします。

  • CHAR_BIT == 8
  • std::uint8_tが定義されています。

バイナリデータを処理していて、charの符号の欠如の可能性は問題ではないと想定して、char*std::uint8_t*を前後にキャストすることは移植可能で安全ですか?

説明付きで規格を参照していただければ幸いです。

編集:ありがとう、ジェリー・コフィン。標準からの引用を追加します([basic.lval]、§3.10/ 10):

プログラムが次のタイプのいずれか以外のglvalueを介してオブジェクトの格納された値にアクセスしようとした場合の動作は未定義です。

...

— charまたはunsigned char型。

EDIT2:わかりました。 std::uint8_tunsigned charのtypedefであるとは限りません。 拡張された符号なし整数型として実装でき、拡張された符号なし整数型は§3.10/ 10に含まれていません。今何?

59
Lyberta

さて、本当の知識を得ましょう。 thisthis 、および this を読んだ後、両方の標準の背後にある意図を理解していることを確信しています。

したがって、reinterpret_castからstd::uint8_t*char*を実行し、結果のポインタを逆参照すると、safeおよびportableそして [basic.lval] によって明示的に許可されています。

ただし、reinterpret_castchar*からstd::uint8_t*に変更し、結果のポインターを逆参照すると、厳密なエイリアス規則に違反し、になります。未定義の動作std::uint8_t拡張された符号なし整数型として実装されている場合。

ただし、最初に次の2つの回避策があります。

static_assert(std::is_same_v<std::uint8_t, char> ||
    std::is_same_v<std::uint8_t, unsigned char>,
    "This library requires std::uint8_t to be implemented as char or unsigned char.");

このアサートが設定されていると、コードは、未定義の動作が発生するプラットフォームではコンパイルされません。

第二:

std::memcpy(uint8buffer, charbuffer, size);

Cppreference は、std::memcpyunsigned charの配列としてオブジェクトにアクセスするため、safeおよびportableであることを示します

繰り返しますが、reinterpret_castchar*の間でstd::uint8_t*を実行し、結果のポインタportablysafely100%標準に準拠した方法で、次の条件を満たしている必要があります。

  • CHAR_BIT == 8
  • std::uint8_tが定義されています。
  • std::uint8_tは、charまたはunsigned charとして実装されます。

実際には、上記の条件はプラットフォームの99%に当てはまり、最初の2つの条件が当てはまるプラットフォームと、3番目の条件が当てはまるプラットフォームはおそらくないでしょう。

24
Lyberta

uint8_tが存在する場合、基本的に唯一の選択肢は、それがunsigned char(または、たまたま署名されていない場合はchar)のtypedefであるということです。何も(ただしビットフィールドは)charよりも少ないストレージを表すことができず、8ビットまで小さくできる唯一の他のタイプはboolです。次に小さい通常の整数型はshortであり、少なくとも16ビットでなければなりません。

そのため、uint8_tがまったく存在する場合、実際に2つの可能性しかありません:unsigned charunsigned charにキャストするか、signed charunsigned charにキャストする。

前者はID変換なので、明らかに安全です。後者は、§3.10/ 10でcharまたはunsigned charのシーケンスとして他のタイプにアクセスするために与えられた「特別な規定」に該当するため、定義された動作も提供します。

これにはcharunsigned charの両方が含まれるため、charのシーケンスとしてそれにアクセスするためのキャストも、定義された動作を提供します。

編集:Lucが拡張整数型について言及している限り、この場合にどうやってそれを適用して違いを得ることができるかはわかりません。 C++はuint8_tなどの定義についてC99標準を参照しているため、これ以降の引用はC99から引用されます。

§6.2.6.1/ 3は、unsigned charがパディングビットなしの純粋なバイナリ表現を使用することを指定しています。埋め込みビットは、6.2.6.2/1でのみ許可されます。これにより、unsigned charが明確に除外されます。ただし、このセクションでは、純粋なバイナリ表現について詳しく説明しています。したがって、unsigned charおよびuint8_t(存在する場合)は、ビットレベルで同じように表す必要があります。

2つの違いを確認するには、特定のビットを一方として表示すると、他方として表示した場合とは異なる結果が得られると主張する必要があります。

より直接的に言えば、2つの結果の違いは、ビットを同じように解釈するという直接的な要件にもかかわらず、ビットを異なる方法で解釈することを要求します。

純粋に理論的なレベルでさえ、これを達成するのは難しいようです。実用レベルに近づくと、明らかにばかげています。

19
Jerry Coffin