{fmt} (バリデーション)
ライブラリ
{fmt} (バリデーション)
概要
{fmt}は、C++20のstd::formatの基盤となったモダンなC++フォーマットライブラリです。主に文字列フォーマットに特化していますが、型安全性とコンパイル時チェック機能により、強力なバリデーション機能を提供します。フォーマット文字列の正確性をコンパイル時に検証し、実行時エラーを大幅に削減します。Python風のf-stringライクな構文と、printf/iostreamを上回る高いパフォーマンスにより、現代的なC++アプリケーションにおける文字列処理のスタンダードとして位置づけられています。
詳細
{fmt}ライブラリは、C++20で標準化されたstd::formatの原型となった実装で、型安全な文字列フォーマットとバリデーション機能を提供します。コンパイル時にフォーマット文字列と引数の型の整合性をチェックし、型不整合やバッファオーバーフローを防止します。カスタム型のフォーマッタを定義することで、独自のバリデーションロジックを組み込むことができ、複雑なデータ構造の安全な文字列化が可能です。実行時パフォーマンスも優秀で、従来のprintf系関数やiostreamと比較して高速な処理を実現しています。
主な特徴
- 型安全性: コンパイル時の型チェックによる安全なフォーマット
- 高性能: printf/iostreamを上回る実行速度
- バッファ安全: 自動メモリ管理によるオーバーフロー防止
- カスタマイズ性: ユーザー定義型のフォーマッタ対応
- 標準準拠: C++20 std::formatの基盤実装
- 広範囲サポート: C++11以降の幅広いコンパイラ対応
メリット・デメリット
メリット
- コンパイル時型チェックによる高い安全性
- バッファオーバーフロー等のセキュリティ問題の防止
- printf/iostreamより高速な実行性能
- 読みやすくPython風の直感的な構文
- 豊富なフォーマットオプション
- C++20標準の基盤となった実績と信頼性
デメリット
- 主にフォーマット処理に特化(汎用バリデーションライブラリではない)
- 複雑なスキーマバリデーション機能は提供しない
- 学習コストが存在(特にカスタムフォーマッタの作成)
- 大規模なライブラリ(単純な用途には機能過多)
- 従来のprintf系コードからの移行作業が必要
参考ページ
書き方の例
インストールと基本セットアップ
# vcpkgを使用してインストール
vcpkg install fmt
# Conanを使用してインストール
conan install fmt/8.1.1@
# CMakeでのfind_package使用例
find_package(fmt REQUIRED)
target_link_libraries(your_target fmt::fmt)
# パッケージマネージャーを使用しない場合
git clone https://github.com/fmtlib/fmt.git
cd fmt
mkdir build && cd build
cmake ..
make -j4
基本的な型安全フォーマットとバリデーション
#include <fmt/format.h>
#include <fmt/printf.h>
#include <fmt/chrono.h>
#include <iostream>
#include <string>
#include <vector>
#include <chrono>
int main() {
// 基本的な型安全フォーマット
{
int number = 42;
std::string name = "Alice";
// 型安全なフォーマット(コンパイル時チェック)
std::string message = fmt::format("ユーザー {} の番号は {} です", name, number);
fmt::print("{}\n", message);
// 位置指定フォーマット
std::string formatted = fmt::format("{1} years ago, {0} was born", name, 25);
fmt::print("{}\n", formatted);
}
// 数値フォーマットとバリデーション
{
double pi = 3.14159265359;
int value = 1234567;
// 小数点以下の桁数指定
fmt::print("円周率: {:.2f}\n", pi);
// 右寄せ、幅指定
fmt::print("値: {:>10}\n", value);
// 左寄せ、0埋め
fmt::print("値: {:0>10}\n", value);
// 中央寄せ
fmt::print("値: {:^10}\n", value);
// 千の位区切り
fmt::print("値: {:n}\n", value);
}
// 文字列フォーマットとバリデーション
{
std::string text = "Hello";
// 幅指定と配置
fmt::print("|{:10}|\n", text); // 右寄せ
fmt::print("|{:<10}|\n", text); // 左寄せ
fmt::print("|{:^10}|\n", text); // 中央寄せ
fmt::print("|{:>10}|\n", text); // 右寄せ(明示的)
// 切り詰め
std::string long_text = "This is a very long text";
fmt::print("切り詰め: {:.10}\n", long_text);
}
// 時間フォーマットとバリデーション
{
auto now = std::chrono::system_clock::now();
auto time_t = std::chrono::system_clock::to_time_t(now);
// 時間のフォーマット
fmt::print("現在時刻: {:%Y-%m-%d %H:%M:%S}\n",
fmt::localtime(time_t));
// 期間のフォーマット
auto duration = std::chrono::milliseconds(1234567);
fmt::print("期間: {}\n", duration);
}
return 0;
}
高度な型安全性とエラーハンドリング
#include <fmt/format.h>
#include <fmt/compile.h>
#include <fmt/args.h>
#include <iostream>
#include <string>
#include <vector>
#include <map>
#include <stdexcept>
// カスタムエラーハンドラー
class ValidationError : public std::runtime_error {
public:
ValidationError(const std::string& message) : std::runtime_error(message) {}
};
// 型安全なフォーマット関数
template<typename... Args>
std::string safe_format(const std::string& format_str, Args&&... args) {
try {
return fmt::format(format_str, std::forward<Args>(args)...);
} catch (const fmt::format_error& e) {
throw ValidationError(fmt::format("フォーマットエラー: {}", e.what()));
}
}
// 条件付きフォーマット
template<typename T>
std::string format_with_validation(const T& value, const std::string& format_str) {
try {
return fmt::format(format_str, value);
} catch (const fmt::format_error& e) {
throw ValidationError(fmt::format("値 {} のフォーマットに失敗: {}",
value, e.what()));
}
}
// 複雑なデータ構造の検証付きフォーマット
struct UserInfo {
std::string name;
int age;
double salary;
std::vector<std::string> skills;
std::map<std::string, std::string> metadata;
};
class UserFormatter {
public:
// 基本情報のフォーマット
static std::string formatBasicInfo(const UserInfo& user) {
// 入力値の検証
if (user.name.empty()) {
throw ValidationError("ユーザー名が空です");
}
if (user.age < 0 || user.age > 150) {
throw ValidationError(fmt::format("無効な年齢: {}", user.age));
}
if (user.salary < 0) {
throw ValidationError(fmt::format("無効な給与: {}", user.salary));
}
return fmt::format("ユーザー: {} ({}歳, 給与: {:,.2f}円)",
user.name, user.age, user.salary);
}
// 詳細情報のフォーマット
static std::string formatDetailedInfo(const UserInfo& user) {
std::string basic = formatBasicInfo(user);
// スキルの検証とフォーマット
std::string skills_str;
if (!user.skills.empty()) {
skills_str = fmt::format("スキル: [{}]", fmt::join(user.skills, ", "));
} else {
skills_str = "スキル: なし";
}
// メタデータの検証とフォーマット
std::string metadata_str;
if (!user.metadata.empty()) {
std::vector<std::string> metadata_parts;
for (const auto& [key, value] : user.metadata) {
if (key.empty()) {
throw ValidationError("メタデータキーが空です");
}
metadata_parts.push_back(fmt::format("{}={}", key, value));
}
metadata_str = fmt::format("メタデータ: [{}]",
fmt::join(metadata_parts, ", "));
} else {
metadata_str = "メタデータ: なし";
}
return fmt::format("{}\n{}\n{}", basic, skills_str, metadata_str);
}
// HTMLフォーマット
static std::string formatAsHtml(const UserInfo& user) {
std::string basic = formatBasicInfo(user);
// HTMLエスケープ処理
auto escape_html = [](const std::string& text) -> std::string {
std::string result;
for (char c : text) {
switch (c) {
case '<': result += "<"; break;
case '>': result += ">"; break;
case '&': result += "&"; break;
case '"': result += """; break;
case '\'': result += "'"; break;
default: result += c; break;
}
}
return result;
};
return fmt::format(
R"(<div class="user-info">
<h3>{}</h3>
<p>年齢: {}</p>
<p>給与: {:,.2f}円</p>
<p>スキル: {}</p>
</div>)",
escape_html(user.name),
user.age,
user.salary,
escape_html(fmt::join(user.skills, ", "))
);
}
};
int main() {
try {
// 基本的な型安全フォーマット
{
std::string name = "田中太郎";
int age = 30;
double salary = 5000000.50;
std::string result = safe_format("ユーザー: {} ({}歳, 給与: {:,.2f}円)",
name, age, salary);
fmt::print("{}\n", result);
}
// 複雑なデータ構造の検証付きフォーマット
{
UserInfo user = {
"佐藤花子",
28,
4500000.75,
{"C++", "Python", "JavaScript", "SQL"},
{{"department", "Engineering"}, {"location", "Tokyo"}, {"level", "Senior"}}
};
std::string basic = UserFormatter::formatBasicInfo(user);
fmt::print("基本情報:\n{}\n\n", basic);
std::string detailed = UserFormatter::formatDetailedInfo(user);
fmt::print("詳細情報:\n{}\n\n", detailed);
std::string html = UserFormatter::formatAsHtml(user);
fmt::print("HTML形式:\n{}\n\n", html);
}
// エラーハンドリングの例
{
UserInfo invalid_user = {
"", // 空の名前
-5, // 無効な年齢
1000000.0,
{},
{}
};
try {
std::string result = UserFormatter::formatBasicInfo(invalid_user);
fmt::print("{}\n", result);
} catch (const ValidationError& e) {
fmt::print("バリデーションエラー: {}\n", e.what());
}
}
// 動的フォーマット文字列の検証
{
std::vector<std::string> format_templates = {
"値: {}",
"値: {:.2f}",
"値: {:>10}",
"無効: {:.2f} と {}" // 引数不足でエラー
};
double value = 123.456;
for (const auto& template_str : format_templates) {
try {
std::string result = format_with_validation(value, template_str);
fmt::print("成功: {}\n", result);
} catch (const ValidationError& e) {
fmt::print("エラー: {}\n", e.what());
}
}
}
} catch (const std::exception& e) {
fmt::print("予期しないエラー: {}\n", e.what());
return 1;
}
return 0;
}
コンパイル時最適化とカスタムフォーマッタ
#include <fmt/format.h>
#include <fmt/compile.h>
#include <fmt/ostream.h>
#include <iostream>
#include <string>
#include <vector>
#include <chrono>
#include <regex>
// カスタム型の定義
struct Point {
double x, y;
Point(double x, double y) : x(x), y(y) {}
// 検証機能付きのコンストラクタ
static Point createValidated(double x, double y) {
if (std::isnan(x) || std::isnan(y)) {
throw std::invalid_argument("座標にNaNが含まれています");
}
if (std::isinf(x) || std::isinf(y)) {
throw std::invalid_argument("座標に無限大が含まれています");
}
return Point(x, y);
}
// 距離の計算
double distance() const {
return std::sqrt(x * x + y * y);
}
// 検証機能
bool isValid() const {
return !std::isnan(x) && !std::isnan(y) &&
!std::isinf(x) && !std::isinf(y);
}
};
// Pointのカスタムフォーマッタ
template<>
struct fmt::formatter<Point> {
enum class format_type { coordinate, polar, json };
format_type type = format_type::coordinate;
int precision = 2;
// フォーマット文字列の解析
constexpr auto parse(format_parse_context& ctx) {
auto it = ctx.begin();
auto end = ctx.end();
if (it != end) {
switch (*it) {
case 'c': type = format_type::coordinate; break;
case 'p': type = format_type::polar; break;
case 'j': type = format_type::json; break;
default:
throw fmt::format_error("無効なフォーマットタイプ");
}
++it;
}
// 精度の解析
if (it != end && *it == '.') {
++it;
if (it != end && *it >= '0' && *it <= '9') {
precision = *it - '0';
++it;
}
}
return it;
}
// フォーマット実行
template<typename FormatContext>
auto format(const Point& p, FormatContext& ctx) {
// 値の検証
if (!p.isValid()) {
return fmt::format_to(ctx.out(), "Invalid Point");
}
switch (type) {
case format_type::coordinate:
return fmt::format_to(ctx.out(), "({:.{}f}, {:.{}f})",
p.x, precision, p.y, precision);
case format_type::polar: {
double r = p.distance();
double theta = std::atan2(p.y, p.x);
return fmt::format_to(ctx.out(), "r={:.{}f}, θ={:.{}f}",
r, precision, theta, precision);
}
case format_type::json:
return fmt::format_to(ctx.out(), "{{\"x\":{:.{}f},\"y\":{:.{}f}}}",
p.x, precision, p.y, precision);
}
return ctx.out();
}
};
// 複雑なデータ構造
struct GeometryData {
std::string name;
std::vector<Point> points;
std::string color;
double area;
// 検証機能
bool isValid() const {
if (name.empty()) return false;
if (points.empty()) return false;
if (area < 0) return false;
for (const auto& point : points) {
if (!point.isValid()) return false;
}
// 色の検証(HTML色コード)
std::regex color_regex("^#([A-Fa-f0-9]{6}|[A-Fa-f0-9]{3})$");
if (!std::regex_match(color, color_regex)) {
return false;
}
return true;
}
};
// GeometryDataのカスタムフォーマッタ
template<>
struct fmt::formatter<GeometryData> {
enum class format_type { simple, detailed, json };
format_type type = format_type::simple;
constexpr auto parse(format_parse_context& ctx) {
auto it = ctx.begin();
auto end = ctx.end();
if (it != end) {
switch (*it) {
case 's': type = format_type::simple; break;
case 'd': type = format_type::detailed; break;
case 'j': type = format_type::json; break;
default:
throw fmt::format_error("無効なフォーマットタイプ");
}
++it;
}
return it;
}
template<typename FormatContext>
auto format(const GeometryData& geo, FormatContext& ctx) {
if (!geo.isValid()) {
return fmt::format_to(ctx.out(), "Invalid GeometryData");
}
switch (type) {
case format_type::simple:
return fmt::format_to(ctx.out(), "{} ({}点, 面積: {:.2f})",
geo.name, geo.points.size(), geo.area);
case format_type::detailed: {
auto out = fmt::format_to(ctx.out(),
"図形: {}\n色: {}\n面積: {:.2f}\n頂点数: {}\n頂点座標:\n",
geo.name, geo.color, geo.area, geo.points.size());
for (size_t i = 0; i < geo.points.size(); ++i) {
out = fmt::format_to(out, " {}: {:.2f}\n", i + 1, geo.points[i]);
}
return out;
}
case format_type::json: {
auto out = fmt::format_to(ctx.out(),
"{{\"name\":\"{}\",\"color\":\"{}\",\"area\":{:.2f},\"points\":[",
geo.name, geo.color, geo.area);
for (size_t i = 0; i < geo.points.size(); ++i) {
if (i > 0) out = fmt::format_to(out, ",");
out = fmt::format_to(out, "{:j}", geo.points[i]);
}
return fmt::format_to(out, "]}");
}
}
return ctx.out();
}
};
// パフォーマンステスト
class PerformanceTest {
public:
static void testCompiledFormat() {
constexpr auto compiled_format = fmt::compile("値: {}, 名前: {}");
auto start = std::chrono::high_resolution_clock::now();
for (int i = 0; i < 100000; ++i) {
std::string result = fmt::format(compiled_format, i, "test");
}
auto end = std::chrono::high_resolution_clock::now();
auto duration = std::chrono::duration_cast<std::chrono::microseconds>(end - start);
fmt::print("コンパイル済みフォーマット: {} μs\n", duration.count());
}
static void testRuntimeFormat() {
const std::string format_str = "値: {}, 名前: {}";
auto start = std::chrono::high_resolution_clock::now();
for (int i = 0; i < 100000; ++i) {
std::string result = fmt::format(format_str, i, "test");
}
auto end = std::chrono::high_resolution_clock::now();
auto duration = std::chrono::duration_cast<std::chrono::microseconds>(end - start);
fmt::print("実行時フォーマット: {} μs\n", duration.count());
}
};
int main() {
try {
// カスタムフォーマッタの使用例
{
Point p1 = Point::createValidated(3.14159, 2.71828);
Point p2 = Point::createValidated(std::nan(""), 1.0); // 無効な座標
fmt::print("座標形式: {:.3f}\n", p1);
fmt::print("極座標形式: {:p.3f}\n", p1);
fmt::print("JSON形式: {:j.3f}\n", p1);
fmt::print("無効な座標: {:.2f}\n", p2);
}
// 複雑なデータ構造のフォーマット
{
GeometryData triangle = {
"三角形",
{Point(0, 0), Point(3, 0), Point(1.5, 2.6)},
"#FF0000",
3.9
};
fmt::print("シンプル形式:\n{:s}\n\n", triangle);
fmt::print("詳細形式:\n{:d}\n", triangle);
fmt::print("JSON形式:\n{:j}\n\n", triangle);
}
// 無効なデータのテスト
{
GeometryData invalid_geo = {
"", // 空の名前
{Point(1, 1)},
"invalid_color", // 無効な色
-1.0 // 負の面積
};
fmt::print("無効なデータ: {:s}\n", invalid_geo);
}
// パフォーマンステスト
{
fmt::print("パフォーマンステスト結果:\n");
PerformanceTest::testCompiledFormat();
PerformanceTest::testRuntimeFormat();
}
} catch (const std::exception& e) {
fmt::print("エラー: {}\n", e.what());
return 1;
}
return 0;
}
国際化対応と高度なバリデーション
#include <fmt/format.h>
#include <fmt/locale.h>
#include <fmt/chrono.h>
#include <iostream>
#include <string>
#include <locale>
#include <map>
#include <regex>
#include <optional>
// 多言語対応のメッセージクラス
class LocalizedMessages {
private:
std::map<std::string, std::map<std::string, std::string>> messages;
std::string current_locale;
public:
LocalizedMessages() : current_locale("ja") {
// 日本語メッセージ
messages["ja"] = {
{"validation_error", "バリデーションエラー: {}"},
{"invalid_email", "無効なメールアドレス: {}"},
{"invalid_phone", "無効な電話番号: {}"},
{"invalid_date", "無効な日付: {}"},
{"required_field", "必須フィールド '{}' が空です"},
{"out_of_range", "値 {} は範囲 {} - {} 外です"},
{"user_info", "ユーザー: {} ({}歳, {})"}
};
// 英語メッセージ
messages["en"] = {
{"validation_error", "Validation error: {}"},
{"invalid_email", "Invalid email address: {}"},
{"invalid_phone", "Invalid phone number: {}"},
{"invalid_date", "Invalid date: {}"},
{"required_field", "Required field '{}' is empty"},
{"out_of_range", "Value {} is out of range {} - {}"},
{"user_info", "User: {} ({}y/o, {})"}
};
}
void setLocale(const std::string& locale) {
if (messages.find(locale) != messages.end()) {
current_locale = locale;
}
}
template<typename... Args>
std::string format(const std::string& key, Args&&... args) {
auto locale_it = messages.find(current_locale);
if (locale_it == messages.end()) {
throw std::runtime_error("未サポートのロケール: " + current_locale);
}
auto message_it = locale_it->second.find(key);
if (message_it == locale_it->second.end()) {
throw std::runtime_error("未定義のメッセージキー: " + key);
}
return fmt::format(message_it->second, std::forward<Args>(args)...);
}
};
// 高度なバリデーション機能
class AdvancedValidator {
private:
LocalizedMessages messages;
// メールアドレスの検証
bool isValidEmail(const std::string& email) {
static const std::regex email_regex(
R"(^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}$)"
);
return std::regex_match(email, email_regex);
}
// 電話番号の検証(日本の形式)
bool isValidPhoneNumber(const std::string& phone) {
static const std::regex phone_regex(
R"(^(\+81|0)[1-9][0-9]{8,9}$)"
);
return std::regex_match(phone, phone_regex);
}
// 日付の検証
bool isValidDate(const std::string& date) {
static const std::regex date_regex(
R"(^\d{4}-\d{2}-\d{2}$)"
);
return std::regex_match(date, date_regex);
}
public:
void setLocale(const std::string& locale) {
messages.setLocale(locale);
}
// 文字列フィールドの検証
std::optional<std::string> validateString(const std::string& value,
const std::string& field_name,
bool required = true,
size_t min_length = 0,
size_t max_length = std::numeric_limits<size_t>::max()) {
if (value.empty() && required) {
return messages.format("required_field", field_name);
}
if (!value.empty() && (value.length() < min_length || value.length() > max_length)) {
return fmt::format("フィールド '{}' の長さは {} - {} 文字である必要があります",
field_name, min_length, max_length);
}
return std::nullopt;
}
// 数値の範囲検証
template<typename T>
std::optional<std::string> validateRange(const T& value,
const std::string& field_name,
const T& min_val,
const T& max_val) {
if (value < min_val || value > max_val) {
return messages.format("out_of_range", value, min_val, max_val);
}
return std::nullopt;
}
// メールアドレスの検証
std::optional<std::string> validateEmail(const std::string& email) {
if (!isValidEmail(email)) {
return messages.format("invalid_email", email);
}
return std::nullopt;
}
// 電話番号の検証
std::optional<std::string> validatePhoneNumber(const std::string& phone) {
if (!isValidPhoneNumber(phone)) {
return messages.format("invalid_phone", phone);
}
return std::nullopt;
}
// 日付の検証
std::optional<std::string> validateDate(const std::string& date) {
if (!isValidDate(date)) {
return messages.format("invalid_date", date);
}
return std::nullopt;
}
// 複合バリデーション
std::vector<std::string> validateUserData(const std::map<std::string, std::string>& data) {
std::vector<std::string> errors;
// 名前の検証
auto name_it = data.find("name");
if (name_it != data.end()) {
auto error = validateString(name_it->second, "name", true, 2, 50);
if (error) errors.push_back(*error);
} else {
errors.push_back(messages.format("required_field", "name"));
}
// メールアドレスの検証
auto email_it = data.find("email");
if (email_it != data.end()) {
auto error = validateEmail(email_it->second);
if (error) errors.push_back(*error);
} else {
errors.push_back(messages.format("required_field", "email"));
}
// 電話番号の検証(任意)
auto phone_it = data.find("phone");
if (phone_it != data.end() && !phone_it->second.empty()) {
auto error = validatePhoneNumber(phone_it->second);
if (error) errors.push_back(*error);
}
// 年齢の検証
auto age_it = data.find("age");
if (age_it != data.end()) {
try {
int age = std::stoi(age_it->second);
auto error = validateRange(age, "age", 0, 120);
if (error) errors.push_back(*error);
} catch (const std::exception&) {
errors.push_back(fmt::format("年齢 '{}' は無効な数値です", age_it->second));
}
} else {
errors.push_back(messages.format("required_field", "age"));
}
return errors;
}
};
// 使用例
int main() {
try {
AdvancedValidator validator;
// 日本語でのテスト
{
fmt::print("=== 日本語バリデーション ===\n");
validator.setLocale("ja");
std::map<std::string, std::string> user_data = {
{"name", "田中太郎"},
{"email", "[email protected]"},
{"phone", "09012345678"},
{"age", "30"}
};
auto errors = validator.validateUserData(user_data);
if (errors.empty()) {
fmt::print("✓ バリデーション成功: {}\n",
fmt::format("ユーザー: {} ({}歳, {})",
user_data["name"],
user_data["age"],
user_data["email"]));
} else {
fmt::print("✗ バリデーションエラー:\n");
for (const auto& error : errors) {
fmt::print(" - {}\n", error);
}
}
}
// 英語でのテスト
{
fmt::print("\n=== English Validation ===\n");
validator.setLocale("en");
std::map<std::string, std::string> user_data = {
{"name", "A"}, // 短すぎる名前
{"email", "invalid-email"}, // 無効なメール
{"phone", "123"}, // 無効な電話番号
{"age", "150"} // 範囲外の年齢
};
auto errors = validator.validateUserData(user_data);
if (errors.empty()) {
fmt::print("✓ Validation successful\n");
} else {
fmt::print("✗ Validation errors:\n");
for (const auto& error : errors) {
fmt::print(" - {}\n", error);
}
}
}
// 高度なフォーマット機能
{
fmt::print("\n=== 高度なフォーマット ===\n");
// 数値の地域別フォーマット
double large_number = 1234567.89;
fmt::print("日本語形式: {:.2Lf}\n", large_number);
// 時間の地域別フォーマット
auto now = std::chrono::system_clock::now();
auto time_t = std::chrono::system_clock::to_time_t(now);
fmt::print("現在時刻: {:%Y年%m月%d日 %H:%M:%S}\n",
fmt::localtime(time_t));
// 複雑なデータ構造のフォーマット
std::vector<std::string> items = {"りんご", "みかん", "バナナ"};
fmt::print("アイテム: [{}]\n", fmt::join(items, ", "));
// 条件付きフォーマット
bool has_items = !items.empty();
fmt::print("アイテム状況: {}\n",
has_items ? fmt::format("{}個のアイテム", items.size()) : "アイテムなし");
}
} catch (const std::exception& e) {
fmt::print("エラー: {}\n", e.what());
return 1;
}
return 0;
}