파일 업로드
  1. base64hub
  2. 코드로 다루는 Base64
  3. C++
코드

C++에서 Base64 사용하기

C++ 표준 라이브러리에는 C++23까지도 Base64가 없습니다. Python과 대조해 퍼징한 간결한 헤더 전용 구현을 소개하고, 이미 갖고 있을지 모르는 라이브러리의 API도 정리합니다.

헤더 전용 구현 (C++17)

encode는 알파벳과 패딩 플래그를 받으므로 같은 함수로 표준 Base64와 URL-safe 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

디코더는 비트 누산기로 동작합니다. 문자마다 6비트를 더하고, 8비트가 찰 때마다 바이트 하나가 됩니다. 끝에 남는 것이 패딩 비트이며, 이 비트는 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-safe와 패딩 없음

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 참조 구현과 대조했습니다. 불일치는 없었습니다. 문자열 하나를 직접 확인하려면 거부 이유를 하나하나 설명해 주는 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를 써서 파일 하나를 아낄 수 있습니다.

라이브러리인코딩디코딩
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>가 그 사실을 정직하게 드러냅니다. 페이로드가 텍스트라는 것을 안다면 벡터의 반복자로 std::string을 만드세요.

예외 대신 std::optional을 쓰는 이유는 무엇인가요?

사용자 입력에서 온 잘못된 Base64는 예외적인 상황이 아니라 예상되는 경우입니다. std::optional은 호출하는 쪽이 이를 처리하도록 강제하고, 정상 경로에서는 비용이 없습니다. 코드베이스가 예외를 선호한다면 래퍼에서 던지세요.

계속 읽기