よもやもダンプ

気が向いた時にアウトプットしておくところ

バイトデータはどの型で保持すべき?

C++でファイルを取り込むときに,ストリームを開いている時間を短くしたいのとインデックスでデータにランダムアクセスしたいので,ファイルのバイトデータを丸ごと配列やコンテナに格納してから内容を解析している. このバイトデータを自分は std::uint8_t の配列で宣言してきたけど,最近標準ライブラリを眺めているとstd::byte なるものが存在することに気が付いた. 便利そうなので使ってみようと思い,サンプルとして人の書いたコードを探していると, char を使っているもの, unsigned char を使っているものがあったりしてだんだんどれを使うべきか分からなくなってきた.
動作としてはどの型を使っても同じことを実現できるのだけども,意味的に使い分けを区別したいというのが自分のこだわりにあるので,候補となりうる型について特徴を調べてみた.なお,C++17以上の環境での使用を想定している.

char

1バイトのサイズを持つ組み込み型.組み込み型なのでヘッダーを何もインクルードしなくても使える.
数値を扱えるが名前の由来の"Character"の通り本来は文字を表す型なので,intのような数値型と同じように扱っていると変数値を文字列に変換するときなどに文字として解釈されてしまい痛い目に合う.

#include <iostream>

int main()
{
    const unsigned char n{0x21};
    // "!" と出力される
    std::cout << n << std::endl;   
    return 0;
}

ちなみに文字列を保持するのにchar*を使ったりするが,Cのコードとの互換性を気にしないのであればstd::stringstd::string_viewを使うのが良さそう.

unsigned char

おそらく多くの場面で慣習的に使われてきた型.組み込み型の中で1バイトのデータといえばこれが思い浮かぶ. 今時気を付ける場面はほとんどないだろうが,注意としてC++では1バイトが8ビットである保証はない1
unsignedと付いているが結局のところ文字型なので,charと同じように場面によっては変数値が文字として扱われる.

std::int8_t / std::uint8_t

<cstdint>ヘッダーで定義されている整数型.8ビットのサイズであることが保証されている.
ただしこの型が定義されるかは処理系依存であるため,環境によっては使用できないかもしれない(ごく稀とは思うが).
また先述のcharunsigned charの型エイリアスとして実装されることが多いため,やはり文字として値を認識されることがある.

std::byte

<cstdlib>ヘッダーで定義されている型.文字でも数値でもなくビット列としてのバイトデータを表す.
ビット演算用の演算子は用意されているが,数値演算は行えない.数値として扱いたい場合はstd::to_interger<T>()を使う.
enum lass byte : unsigned charとして定義されているため,その値の初期化には波カッコによる初期化かstaticキャストによる代入で行う.

#include <iostream>
#include <cstddef>

int main()
{
    const std::byte a{0b0010'0001};
    const auto b = static_cast<std::byte>(0x11);

    const auto c = a | b;
    // const auto d = a + b; はエラー

    // "0x31" と出力される
    std::cout << "0x" << std::hex << std::to_integer<int>(c) << std::endl;

    return 0;
}

void*

void単体では何の意味もなさないが,バイト列を保持するというケースの場合はvoid*を使う手も考えられる.
コールバック関数の引数に任意のデータを渡す際にこの型を使用しているライブラリを見たことがある.
ただしポインターであれば何でも代入できてしまい,C++の静的型付け言語であるメリットが失われてしまうので扱いには注意が必要.
代わりにオブジェクトの寿命の管理が可能かつ不適切な型へのキャストを制限することができるstd::anystd::variant<T...>を使いたい.

使い分け

で,どう使い分けたらいいのか色々探してみると,良さそうな例がいくつか見つかった.

stackoverflow.com

www.reddit.com

個人的には以下のように使い分けるのが良さそう

  • バイトデータが「メモリ空間上のデータ」であり,そのデータの型や構造は不明または問わない(blobなどのデータの場合)
    std::vector<std::byte>
  • バイトデータが数値の羅列であるとき
    → 符号の有り無しに応じて std::vector<std::int8_t>, std::vector<std::uint8_t> などビット幅を明示した型を使う
  • バイトデータが文字列であるとき
    char*もしくはそこからstd::stringstd::string_viewに変換
  • コールバック関数の引数となる汎用型としてのバイトデータ → 許容する型を明示したい場合はstd::variant,そうでない場合はstd::any
  • バイトデータは特定の構造を持つオブジェクトだが,何らかの理由でその型を隠蔽したい(セキュリティ的な理由?) → std::any

標準ライブラリに追加されるクラスを見た感じ,どうもC++は生ポインターを使わなくてもいいようにしていきたい気がするので,できるだけクラスを使ってバイトデータを扱うように考えてみた.
あくまでも考えてみただけの段階なので,実際コードを書いてみるとこのルールではうまくいかないことが多いかもしれない(特に外部ライブラリを使用する場合). そういったところはunsigned charなども使いながらうまく運用していけたらと思う.


  1. 1バイトが何ビットであるかは<climits>ヘッダーのCHAR_BITで確認できる.