cpp-validator
ライブラリ
cpp-validator
概要
cpp-validatorは、C++14/17対応のモダンなヘッダーオンリーバリデーションライブラリです。変数、オブジェクト、コンテナの検証に特化しており、シンプルで読みやすいAPIを提供します。事前検証と事後検証の両方をサポートし、自動的なエラーメッセージ生成機能とロケール対応により、国際化されたアプリケーション開発に適しています。Clang、GCC、MSVCで動作確認済みで、現代的なC++プロジェクトに簡単に統合できます。
詳細
cpp-validatorは、データ制約を宣言的に定義し、必要に応じて適用するためのライブラリです。バリデーションルールを特定のコードポイントで定義し、他の場所でオンデマンドで適用できる柔軟な設計となっています。エラーメッセージはユーザーのロケールを考慮して自動生成され、複雑な条件付きバリデーションや入れ子のオブジェクト検証も支援します。例外ベースとエラーレポートベースの両方のエラーハンドリングパターンを提供し、開発者の好みに応じて使い分けることができます。
主な特徴
- ヘッダーオンリー: 簡単な統合と配布
- 宣言的API: 読みやすく保守しやすいバリデーション記述
- 多様な検証対象: 変数、オブジェクト、コンテナの包括的な検証
- 自動エラーメッセージ: ロケール対応の自動メッセージ生成
- 柔軟なエラーハンドリング: 例外ベースとレポートベース
- モダンC++: C++14/17機能を活用した設計
メリット・デメリット
メリット
- ヘッダーオンリーライブラリで導入が簡単
- シンプルで直感的なAPIデザイン
- 自動的なエラーメッセージ生成とロケール対応
- 事前検証と事後検証の両方をサポート
- 入れ子のオブジェクトや複雑な条件付きバリデーション対応
- 豊富なカスタマイズオプション
デメリット
- 比較的新しいライブラリで採用実績が少ない
- 大規模なライブラリエコシステムとの統合例が限定的
- 高度なスキーマバリデーション機能は提供しない
- パフォーマンスクリティカルな用途での最適化が不明確
- ドキュメントが他の主要ライブラリと比較して少ない
参考ページ
書き方の例
インストールと基本セットアップ
# Conanを使用してインストール
conan install cpp-validator/1.0@evgeniums/stable
# vcpkgを使用してインストール
vcpkg install cpp-validator
# 手動でヘッダーファイルをダウンロード
wget https://github.com/evgeniums/cpp-validator/archive/refs/heads/master.zip
unzip master.zip
# include/を適切なディレクトリに配置
基本的な変数とオブジェクトの検証
#include <cpp-validator/validator.hpp>
#include <cpp-validator/validate.hpp>
#include <cpp-validator/adapters/reporting.hpp>
#include <iostream>
#include <string>
namespace cv = cpp_validator;
int main() {
// 基本的な数値バリデーション
{
// 100より大きい値のバリデーター
auto v = cv::validator(cv::gt, 100);
int value1 = 90;
if (!v.apply(value1)) {
std::cout << "値 " << value1 << " は100より大きくありません" << std::endl;
}
int value2 = 150;
if (v.apply(value2)) {
std::cout << "値 " << value2 << " は有効です" << std::endl;
}
}
// 文字列バリデーション
{
// 文字列の値と長さのバリデーター
auto v = cv::validator(
cv::value(cv::gte, "sample string"), // 辞書順で "sample string" 以上
cv::size(cv::lt, 15) // 文字数が15未満
);
std::string str1 = "sample";
if (!v.apply(str1)) {
std::cout << "文字列 \"" << str1 << "\" は条件を満たしません" << std::endl;
}
std::string str2 = "sample string+";
if (v.apply(str2)) {
std::cout << "文字列 \"" << str2 << "\" は有効です" << std::endl;
}
std::string str3 = "too long sample string";
if (!v.apply(str3)) {
std::cout << "文字列 \"" << str3 << "\" は長すぎます" << std::endl;
}
}
// 範囲チェック
{
auto v = cv::validator(cv::in, cv::interval(95, 100));
size_t val = 90;
if (!v.apply(val)) {
std::cout << "値 " << val << " は95-100の範囲にありません" << std::endl;
}
size_t val2 = 97;
if (v.apply(val2)) {
std::cout << "値 " << val2 << " は有効な範囲内です" << std::endl;
}
}
return 0;
}
エラーレポートとメッセージ生成
#include <cpp-validator/validator.hpp>
#include <cpp-validator/validate.hpp>
#include <cpp-validator/adapters/reporting.hpp>
#include <iostream>
#include <string>
namespace cv = cpp_validator;
int main() {
// エラーレポートを使用したバリデーション
{
auto v = cv::validator(cv::in, cv::interval(95, 100));
cv::error_report err;
size_t val = 90;
cv::validate(val, v, err);
if (err) {
std::cout << "エラー: " << err.message() << std::endl;
// 出力: "must be in interval [95,100]"
}
}
// 複数条件のバリデーション
{
auto v = cv::validator(
cv::value(cv::ne, "UNKNOWN"), // "UNKNOWN"と等しくない
cv::size(cv::lte, 32) // 長さが32以下
);
cv::error_report err;
std::string val = "UNKNOWN";
cv::validate(val, v, err);
if (err) {
std::cout << "文字列バリデーションエラー: " << err.message() << std::endl;
}
}
// 数値の詳細なバリデーション
{
auto v = cv::validator(
cv::gte(0), // 0以上
cv::lte(1000), // 1000以下
cv::ne(500) // 500と等しくない
);
cv::error_report err;
int val = 500;
cv::validate(val, v, err);
if (err) {
std::cout << "数値バリデーションエラー: " << err.message() << std::endl;
}
}
// カスタムエラーメッセージの設定
{
auto v = cv::validator(
cv::gt(0, "値は正の数である必要があります"),
cv::lt(100, "値は100未満である必要があります")
);
cv::error_report err;
int val = -5;
cv::validate(val, v, err);
if (err) {
std::cout << "カスタムエラー: " << err.message() << std::endl;
}
}
return 0;
}
例外ベースバリデーションと複雑なオブジェクト検証
#include <cpp-validator/validator.hpp>
#include <cpp-validator/validate.hpp>
#include <cpp-validator/adapters/reporting.hpp>
#include <iostream>
#include <string>
#include <vector>
#include <map>
namespace cv = cpp_validator;
// 複雑なデータ構造の例
struct User {
std::string name;
int age;
std::string email;
std::vector<std::string> tags;
std::map<std::string, std::string> metadata;
};
int main() {
// 例外ベースバリデーション
{
auto v = cv::validator(cv::gt, 18);
try {
cv::validate(25, v); // 成功
std::cout << "年齢バリデーション成功" << std::endl;
cv::validate(15, v); // 例外発生
} catch (const cv::validation_error& err) {
std::cout << "年齢バリデーションエラー: " << err.what() << std::endl;
}
}
// 入れ子のオブジェクトバリデーション
{
// ユーザー情報のバリデーションルール
auto userValidator = cv::validator(
// 名前の検証
cv::_(cv::key("name"))(
cv::value(cv::ne, ""), // 空文字でない
cv::size(cv::gte, 2), // 2文字以上
cv::size(cv::lte, 50) // 50文字以下
),
// 年齢の検証
cv::_(cv::key("age"))(
cv::gte(0), // 0以上
cv::lte(120) // 120以下
),
// メールアドレスの検証
cv::_(cv::key("email"))(
cv::size(cv::gte, 5), // 5文字以上
cv::contains("@") // @を含む
)
);
// テスト用のユーザーデータ
std::map<std::string, std::string> user_data = {
{"name", "John Doe"},
{"age", "25"},
{"email", "[email protected]"}
};
try {
cv::validate(user_data, userValidator);
std::cout << "ユーザーデータバリデーション成功" << std::endl;
} catch (const cv::validation_error& err) {
std::cout << "ユーザーデータバリデーションエラー: " << err.what() << std::endl;
}
}
// コンテナバリデーション
{
// 数値のベクター用バリデーター
auto vectorValidator = cv::validator(
cv::size(cv::gte, 1), // 少なくとも1つの要素
cv::size(cv::lte, 10), // 最大10個の要素
cv::each(cv::gte(0)) // 各要素が0以上
);
std::vector<int> numbers = {1, 2, 3, 4, 5};
try {
cv::validate(numbers, vectorValidator);
std::cout << "数値配列バリデーション成功" << std::endl;
} catch (const cv::validation_error& err) {
std::cout << "数値配列バリデーションエラー: " << err.what() << std::endl;
}
// 無効なデータでテスト
std::vector<int> invalid_numbers = {1, -2, 3};
try {
cv::validate(invalid_numbers, vectorValidator);
} catch (const cv::validation_error& err) {
std::cout << "無効な数値配列: " << err.what() << std::endl;
}
}
return 0;
}
高度なバリデーション機能とカスタマイズ
#include <cpp-validator/validator.hpp>
#include <cpp-validator/validate.hpp>
#include <cpp-validator/adapters/reporting.hpp>
#include <iostream>
#include <string>
#include <regex>
#include <functional>
namespace cv = cpp_validator;
// カスタムバリデーション関数
bool isValidEmail(const std::string& email) {
static const std::regex email_pattern(
R"(^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}$)"
);
return std::regex_match(email, email_pattern);
}
bool isValidPhoneNumber(const std::string& phone) {
static const std::regex phone_pattern(
R"(^[\+]?[1-9][\d]{0,15}$)"
);
return std::regex_match(phone, phone_pattern);
}
// 複雑なデータ構造
struct ContactInfo {
std::string name;
std::string email;
std::string phone;
int age;
std::string country;
bool newsletter_subscription;
};
// カスタムバリデーター クラス
class ContactValidator {
private:
std::vector<std::string> valid_countries = {
"Japan", "USA", "UK", "Germany", "France", "Canada"
};
public:
bool validateCountry(const std::string& country) {
return std::find(valid_countries.begin(), valid_countries.end(), country)
!= valid_countries.end();
}
cv::error_report validateContact(const ContactInfo& contact) {
cv::error_report report;
// 名前のバリデーション
auto nameValidator = cv::validator(
cv::size(cv::gte, 2, "名前は2文字以上である必要があります"),
cv::size(cv::lte, 50, "名前は50文字以下である必要があります")
);
cv::validate(contact.name, nameValidator, report);
// メールアドレスのバリデーション
if (!isValidEmail(contact.email)) {
report.add_error("無効なメールアドレス形式です");
}
// 電話番号のバリデーション
if (!contact.phone.empty() && !isValidPhoneNumber(contact.phone)) {
report.add_error("無効な電話番号形式です");
}
// 年齢のバリデーション
auto ageValidator = cv::validator(
cv::gte(13, "年齢は13歳以上である必要があります"),
cv::lte(120, "年齢は120歳以下である必要があります")
);
cv::validate(contact.age, ageValidator, report);
// 国のバリデーション
if (!validateCountry(contact.country)) {
report.add_error("サポートされていない国です");
}
return report;
}
// 条件付きバリデーション
cv::error_report validateContactWithConditions(const ContactInfo& contact) {
cv::error_report report;
// 基本バリデーション
auto basic_report = validateContact(contact);
if (basic_report.has_errors()) {
report.merge(basic_report);
}
// 条件付きバリデーション
if (contact.newsletter_subscription) {
// ニュースレター購読者にはメールアドレスが必須
if (contact.email.empty()) {
report.add_error("ニュースレター購読にはメールアドレスが必要です");
}
}
// 未成年者の場合は特別な制限
if (contact.age < 18) {
if (contact.country != "Japan") {
report.add_error("未成年者は日本在住者のみ登録可能です");
}
}
return report;
}
};
// 一括バリデーション
class BatchValidator {
public:
struct ValidationResult {
bool is_valid;
std::vector<std::string> errors;
size_t valid_count;
size_t invalid_count;
ValidationResult() : is_valid(true), valid_count(0), invalid_count(0) {}
};
ValidationResult validateContactList(const std::vector<ContactInfo>& contacts) {
ValidationResult result;
ContactValidator validator;
for (size_t i = 0; i < contacts.size(); ++i) {
const auto& contact = contacts[i];
auto report = validator.validateContactWithConditions(contact);
if (report.has_errors()) {
result.invalid_count++;
result.is_valid = false;
for (const auto& error : report.errors()) {
result.errors.push_back(
"連絡先[" + std::to_string(i) + "] " + contact.name +
": " + error
);
}
} else {
result.valid_count++;
}
}
return result;
}
};
int main() {
// 単体の連絡先バリデーション
{
ContactInfo contact = {
"John Doe",
"[email protected]",
"+1234567890",
25,
"USA",
true
};
ContactValidator validator;
auto report = validator.validateContactWithConditions(contact);
if (report.has_errors()) {
std::cout << "連絡先バリデーションエラー:" << std::endl;
for (const auto& error : report.errors()) {
std::cout << " - " << error << std::endl;
}
} else {
std::cout << "連絡先バリデーション成功: " << contact.name << std::endl;
}
}
// 複数の連絡先の一括バリデーション
{
std::vector<ContactInfo> contacts = {
{"Alice Smith", "[email protected]", "+1987654321", 30, "Canada", false},
{"Bob Johnson", "invalid-email", "+1555123456", 16, "USA", true},
{"Charlie Brown", "[email protected]", "", 25, "Germany", true},
{"", "[email protected]", "+1444555666", 22, "InvalidCountry", false}
};
BatchValidator batchValidator;
auto result = batchValidator.validateContactList(contacts);
std::cout << "\n一括バリデーション結果:" << std::endl;
std::cout << "有効な連絡先: " << result.valid_count << std::endl;
std::cout << "無効な連絡先: " << result.invalid_count << std::endl;
if (!result.is_valid) {
std::cout << "エラー詳細:" << std::endl;
for (const auto& error : result.errors) {
std::cout << " - " << error << std::endl;
}
}
}
return 0;
}
パフォーマンスとスケーラビリティ
#include <cpp-validator/validator.hpp>
#include <cpp-validator/validate.hpp>
#include <iostream>
#include <chrono>
#include <vector>
#include <random>
#include <string>
namespace cv = cpp_validator;
// パフォーマンステスト用のデータ生成
class TestDataGenerator {
private:
std::random_device rd;
std::mt19937 gen{rd()};
std::uniform_int_distribution<> age_dist{18, 80};
std::uniform_int_distribution<> name_len_dist{5, 20};
std::uniform_int_distribution<> char_dist{'a', 'z'};
public:
struct TestRecord {
std::string name;
int age;
double score;
std::vector<int> values;
};
TestRecord generateRecord() {
TestRecord record;
// ランダムな名前を生成
int name_len = name_len_dist(gen);
record.name.reserve(name_len);
for (int i = 0; i < name_len; ++i) {
record.name += static_cast<char>(char_dist(gen));
}
record.age = age_dist(gen);
record.score = std::uniform_real_distribution<>{0.0, 100.0}(gen);
// ランダムな値の配列を生成
int value_count = std::uniform_int_distribution<>{1, 10}(gen);
record.values.reserve(value_count);
for (int i = 0; i < value_count; ++i) {
record.values.push_back(std::uniform_int_distribution<>{0, 1000}(gen));
}
return record;
}
std::vector<TestRecord> generateRecords(size_t count) {
std::vector<TestRecord> records;
records.reserve(count);
for (size_t i = 0; i < count; ++i) {
records.push_back(generateRecord());
}
return records;
}
};
// 高速バリデーション クラス
class FastValidator {
private:
// 事前コンパイル済みバリデーター
static inline const auto name_validator = cv::validator(
cv::size(cv::gte, 2),
cv::size(cv::lte, 50)
);
static inline const auto age_validator = cv::validator(
cv::gte(18),
cv::lte(120)
);
static inline const auto score_validator = cv::validator(
cv::gte(0.0),
cv::lte(100.0)
);
static inline const auto values_validator = cv::validator(
cv::size(cv::gte, 1),
cv::size(cv::lte, 20),
cv::each(cv::gte(0))
);
public:
bool validateRecord(const TestDataGenerator::TestRecord& record) {
return name_validator.apply(record.name) &&
age_validator.apply(record.age) &&
score_validator.apply(record.score) &&
values_validator.apply(record.values);
}
struct ValidationStats {
size_t total_records;
size_t valid_records;
size_t invalid_records;
double validation_time_ms;
double records_per_second;
};
ValidationStats validateRecords(const std::vector<TestDataGenerator::TestRecord>& records) {
ValidationStats stats;
stats.total_records = records.size();
stats.valid_records = 0;
stats.invalid_records = 0;
auto start = std::chrono::high_resolution_clock::now();
for (const auto& record : records) {
if (validateRecord(record)) {
stats.valid_records++;
} else {
stats.invalid_records++;
}
}
auto end = std::chrono::high_resolution_clock::now();
auto duration = std::chrono::duration_cast<std::chrono::microseconds>(end - start);
stats.validation_time_ms = duration.count() / 1000.0;
stats.records_per_second = (stats.total_records * 1000000.0) / duration.count();
return stats;
}
};
int main() {
// パフォーマンステストの実行
{
TestDataGenerator generator;
FastValidator validator;
// 小規模テスト
std::cout << "小規模テスト (1,000レコード):" << std::endl;
auto small_records = generator.generateRecords(1000);
auto small_stats = validator.validateRecords(small_records);
std::cout << " 総レコード数: " << small_stats.total_records << std::endl;
std::cout << " 有効レコード数: " << small_stats.valid_records << std::endl;
std::cout << " 無効レコード数: " << small_stats.invalid_records << std::endl;
std::cout << " 処理時間: " << small_stats.validation_time_ms << " ms" << std::endl;
std::cout << " 処理速度: " << small_stats.records_per_second << " records/sec" << std::endl;
// 大規模テスト
std::cout << "\n大規模テスト (100,000レコード):" << std::endl;
auto large_records = generator.generateRecords(100000);
auto large_stats = validator.validateRecords(large_records);
std::cout << " 総レコード数: " << large_stats.total_records << std::endl;
std::cout << " 有効レコード数: " << large_stats.valid_records << std::endl;
std::cout << " 無効レコード数: " << large_stats.invalid_records << std::endl;
std::cout << " 処理時間: " << large_stats.validation_time_ms << " ms" << std::endl;
std::cout << " 処理速度: " << large_stats.records_per_second << " records/sec" << std::endl;
// メモリ使用量の最適化例
std::cout << "\nメモリ効率的な処理 (ストリーミング):" << std::endl;
auto start = std::chrono::high_resolution_clock::now();
size_t batch_size = 1000;
size_t total_processed = 0;
size_t total_valid = 0;
for (size_t i = 0; i < 10; ++i) {
auto batch = generator.generateRecords(batch_size);
auto stats = validator.validateRecords(batch);
total_processed += stats.total_records;
total_valid += stats.valid_records;
// バッチ処理後にメモリを解放
batch.clear();
batch.shrink_to_fit();
}
auto end = std::chrono::high_resolution_clock::now();
auto duration = std::chrono::duration_cast<std::chrono::microseconds>(end - start);
std::cout << " 総処理レコード数: " << total_processed << std::endl;
std::cout << " 有効レコード数: " << total_valid << std::endl;
std::cout << " 総処理時間: " << duration.count() / 1000.0 << " ms" << std::endl;
std::cout << " 平均処理速度: " << (total_processed * 1000000.0) / duration.count() << " records/sec" << std::endl;
}
return 0;
}