ファイルを選択
  1. base64hub
  2. 各言語での Base64
  3. C++
コード

C++ での Base64

C++ の標準ライブラリには、C++23 になっても Base64 がありません。このページでは、Python と突き合わせてファジングした小さなヘッダーオンリー実装を示し、すでにお使いかもしれないライブラリの API も一覧にします。

ヘッダーオンリーの実装(C++17)

encode はアルファベットとパディングのフラグを引数に取るので、同じ関数で標準と URLセーフの両方の Base64 を生成できます。decode はパディングの有無にかかわらず受け付けますが、不正な文字、位置のおかしい =、あり得ない長さ、0 でないパディングビットは拒否します。

// base64.hpp
#pragma once
#include <array>
#include <cstdint>
#include <optional>
#include <string>
#include <string_view>
#include <vector>

namespace b64 {

inline constexpr std::string_view kStd =
    "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/";
inline constexpr std::string_view kUrl =
    "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789-_";

inline std::string encode(const void* data, std::size_t n,
                          std::string_view abc = kStd, bool pad = true) {
    auto p = static_cast<const std::uint8_t*>(data);
    std::string out;
    out.reserve((n + 2) / 3 * 4);
    std::size_t i = 0;
    for (; i + 2 < n; i += 3) {
        std::uint32_t v = p[i] << 16 | p[i + 1] << 8 | p[i + 2];
        out += abc[v >> 18 & 63];
        out += abc[v >> 12 & 63];
        out += abc[v >> 6 & 63];
        out += abc[v & 63];
    }
    if (i < n) {
        std::uint32_t v = p[i] << 16 | (i + 1 < n ? p[i + 1] << 8 : 0);
        out += abc[v >> 18 & 63];
        out += abc[v >> 12 & 63];
        if (i + 1 < n) out += abc[v >> 6 & 63];
        if (pad) out.append(i + 1 < n ? 1 : 2, '=');
    }
    return out;
}

inline std::string encode(std::string_view s, std::string_view abc = kStd,
                          bool pad = true) {
    return encode(s.data(), s.size(), abc, pad);
}

inline std::optional<std::vector<std::uint8_t>> decode(
        std::string_view s, std::string_view abc = kStd) {
    std::array<int, 256> val;
    val.fill(-1);
    for (int i = 0; i < 64; ++i) val[static_cast<unsigned char>(abc[i])] = i;

    std::size_t len = s.size();
    if (len % 4 == 0 && len > 0 && s[len - 1] == '=')
        len -= (s[len - 2] == '=') ? 2 : 1;
    if (len % 4 == 1) return std::nullopt;
    if (s.size() != len && s.size() % 4 != 0) return std::nullopt;

    std::vector<std::uint8_t> out;
    out.reserve(len * 3 / 4);
    std::uint32_t acc = 0;
    int bits = 0;
    for (std::size_t i = 0; i < len; ++i) {
        int v = val[static_cast<unsigned char>(s[i])];
        if (v < 0) return std::nullopt;
        acc = (acc << 6) | static_cast<std::uint32_t>(v);
        bits += 6;
        if (bits >= 8) {
            bits -= 8;
            out.push_back(static_cast<std::uint8_t>(acc >> bits));
            acc &= (1u << bits) - 1;
        }
    }
    if (acc != 0) return std::nullopt;  // leftover bits must be zero
    return out;
}

}  // namespace b64

デコーダーはビットアキュムレーターとして動作します。1 文字ごとに 6 ビットを加え、8 ビットそろうたびに 1 バイトにします。最後に残るのがパディングビットで、これは 0 でなければなりません。その理由は Base64 の仕組み で説明しています。

文字列のエンコードとデコード

#include "base64.hpp"
#include <iostream>

int main() {
    std::string enc = b64::encode("héllo wörld");
    std::cout << enc << '\n';                         // aMOpbGxvIHfDtnJsZA==

    if (auto bytes = b64::decode(enc))
        std::cout << std::string(bytes->begin(), bytes->end()) << '\n';  // héllo wörld
}

std::string が保持するのはバイト列です。ソースファイルが UTF-8 なら(GCC と Clang のデフォルト。MSVC では /utf-8 が必要)、リテラルは UTF-8 になり、出力は他の言語と一致します。C++20 の std::u8string では、s.data() と s.size() をポインター版のオーバーロードに渡してください。

URLセーフとパディングなし

std::string q = "<<?\?\?>>";                          // "<<???>>", escaped (see below)
b64::encode(q, b64::kUrl);                            // PDw_Pz8-Pg==
b64::encode(q, b64::kUrl, false);                     // PDw_Pz8-Pg
b64::decode("PDw_Pz8-Pg", b64::kUrl);                 // bytes of "<<???>>"

エスケープしているのは、C++17 より前の C++ では ??> がトライグラフだからです。GCC は厳格な -std=c++11 / c++14 モードでこれを置き換えます。JWT やほとんどのトークンはパディングなしの Base64URL を使います。Base64 と Base64URL の違い をご覧ください。

検証結果

decode の戻り値(すべて検証済み):

入力結果
aGk=2 バイト
aGk2 バイト(パディングなしも受理)
aA== / aA1 バイト
aGl=nullopt(パディングビットが 0 でない)
aGk==nullopt(長さが不正)
a===nullopt(パディングが多すぎる)
aGk=!!nullopt
PDw_Pz8-Pg==kStd では nullopt

g++ -std=c++17 -Wall -Wextra -Wpedantic -fsanitize=address,undefined でビルドし、両方のアルファベットで 5,000 個のランダムなバッファを往復変換しました。エンコーダーは Python の b64encode と比較し、30,000 個のランダムな文字列について受理・拒否の判定を Python のリファレンス実装と照合しました。不一致はありませんでした。1 つの文字列を手作業で確認するなら、Base64 バリデーター が拒否の理由を個別に説明します。

ファイル

#include <fstream>
#include <iterator>

std::ifstream f("hello.txt", std::ios::binary);
std::string data((std::istreambuf_iterator<char>(f)), std::istreambuf_iterator<char>());
std::cout << b64::encode(data) << '\n';               // aGVsbG8K

auto raw = b64::decode("aGVsbG8K");
std::ofstream("copy.txt", std::ios::binary)
    .write(reinterpret_cast<const char*>(raw->data()),
           static_cast<std::streamsize>(raw->size()));

Windows では std::ios::binary が重要です。テキストモードは改行コードを変換するため、バイナリデータが壊れます。大きなファイルは 3 の倍数サイズのブロックで読み込み、それぞれに encode を呼べば、出力をそのままつなげられます。そのループは C での Base64 で紹介しています。結果を埋め込み用の C 配列にしたい場合は、Base64 からバイト配列へ ツールで生成できます。

すでに使っているかもしれないライブラリ

これらのいずれかがすでに依存関係にあるなら、その Base64 API を使えばファイルを 1 つ減らせます。

ライブラリエンコードデコード
OpenSSLEVP_EncodeBlockEVP_DecodeBlock(パディング分を含む長さを返す)
Abseilabsl::Base64Escape、absl::WebSafeBase64Escapeabsl::Base64Unescape、absl::WebSafeBase64Unescape
QtQByteArray::toBase64(QByteArray::Base64UrlEncoding | QByteArray::OmitTrailingEquals)QByteArray::fromBase64
PocoPoco::Base64Encoder(ostream)Poco::Base64Decoder(istream)
Boostboost::beast::detail::base64(内部用で、安定した API ではない)同上

パディングや空白の扱いはライブラリごとに異なるため、検証に使う前にそれぞれ確認してください。たとえば EVP_DecodeBlock は戻り値からパディング分を差し引かないので、aGk= に対して 2 バイトではなく 3 バイトと報告します。

落とし穴

  • 符号付き char をインデックスに使う。 char が符号付きの場合、127 を超えるバイトに対する table[s[i]] は未定義動作です。上のコードのように unsigned char にキャストしてください。
  • デコードしたバイト列を C 文字列として扱う。 デコードしたバイト列には \0 が含まれることがあります。strlen や、長さを取らずに const char* だけを受け取る API には決して渡さないでください。
  • バッファサイズを n * 4 / 3 で見積もる。 最大 3 文字足りなくなります。正確な式は サイズ増加の解説 にあります。

よくある質問

C++ に標準の Base64 関数はありますか?

ありません。標準ライブラリにも、C++23 までに採択された提案にも含まれていません。このページのような小さな実装を使うか、プロジェクトがすでに依存しているライブラリの API を使ってください。

decode の戻り値は std::string と std::vector<uint8_t> のどちらにすべきですか?

Base64 をデコードした結果はバイナリなので、std::vector<std::uint8_t> のほうが実態を正直に表します。中身がテキストだとわかっているなら、vector のイテレーターから std::string を構築してください。

例外ではなく std::optional を使うのはなぜですか?

ユーザー入力が不正な Base64 であるのは想定内のケースで、例外的な事態ではありません。std::optional なら呼び出し側に処理を強制でき、正常系ではコストもかかりません。コードベースが例外を好むなら、ラッパーから throw してください。

続けて読む