toge's diary

コンピュータ関連の趣味をつらつらと。

glazeで環境変数から構造体に値を読み取る

glazeで環境変数から構造体に値を読み取る

前回の続きです。 glazeのリフレクション機能がだんだん分かってきたので、すこし実用的なコードを書いてみました。

やりたいこと

集成体の各メンバーに環境変数から値を取得して代入する

という関数を書いてみようと思います。

struct test_struct {
  int number1;
  float number2;
  std::string message;
};

という構造体を定義しているとして、 NUMBER_1, NUMBER_2, MESSAGEという環境変数の値を取得して、対応する型変換をして代入する、というのが期待する動作です。

  1. フィールドの名前を取得する
  2. フィールドの型を確認する
  3. 環境変数値が文字列で取得できるので、変換処理を書いて代入する

というのを全フィールドに対して実装する必要がありますね。 毎回これを書くのはめんどうなのでglazeの機能でできるだけ実装してしまおうという話です。

メンバー名を一覧表示する

前回の投稿のおさらいでメンバーの一覧を取得して表示する関数を作ってみます。 メタ情報あり・なしの両方で一覧表示できるようにしてみます。

#include <iostream>
#include "glaze/glaze.hpp"

struct test_struct {
  int number1;
  float number2;
  std::string message;
};

template<>
struct glz::meta<test_struct> {
  using T = test_struct;
  static constexpr auto value = glz::object(
    "NUMBER_1", &T::number1,
    "NUMBER_2", &T::number2,
    "MESSAGE", &T::message
  );
};

template<typename T, std::size_t IDX = 0>
auto print_members(T const& value) {
  if constexpr (IDX < glz::reflect<T>::size) {
    auto const field_name = glz::reflect<T>::keys[IDX];
    std::cout << IDX << " " << field_name << '\n';
    print_members<T, IDX + 1>(value);
  }
}

auto main([[maybe_unused]] int argc, [[maybe_unused]] char* argv[]) -> int {
  auto value = test_struct{};
  print_members(value);
}

実行結果は以下のとおり。 フィールド名がとれていることが確認できます。

0 NUMBER_1
1 NUMBER_2
2 MESSAGE

泥臭くてすみませんが、再帰関数での実装しか思いつきませんでした。 きっともっと綺麗な方法があるはず。昔の方法しか知らないのです…。

メンバーのサイズを取得した上で0から順に増加させていき、そのインデックスのメンバー情報を取得しています。

メンバーの値も表示する

これも前回の投稿の応用ですね。 メタ情報のあるなしで実装方法が変わるのでif constexprで分岐させています。

#include <iostream>
#include "glaze/glaze.hpp"

struct test_struct {
  int number1;
  float number2;
  std::string message;
};

template<>
struct glz::meta<test_struct> {
  using T = test_struct;
  static constexpr auto value = glz::object(
    "NUMBER_1", &T::number1,
    "NUMBER_2", &T::number2,
    "MESSAGE", &T::message
  );
};

template<typename T, std::size_t IDX = 0>
auto print_members(T const& value) {
  if constexpr (IDX < glz::reflect<T>::size) {
    auto const field_name = glz::reflect<T>::keys[IDX];
    auto const& field_value = [&]() -> decltype(auto) {
        if constexpr (glz::reflectable<T>) {
          // メタ情報がない場合はstd::tupleにしてから取得する
          return glz::get<IDX>(glz::to_tie(value));
        } else {
          // メタ情報がある場合はメタ情報を利用して取得する
          auto& member = glz::get<IDX>(glz::reflect<T>::values);
          return glz::get_member(value, member);
        }
    }();
    std::cout << IDX << " " << field_name << " " << field_value << '\n';

    print_members<T, IDX + 1>(value);
  }
}

auto main([[maybe_unused]] int argc, [[maybe_unused]] char* argv[]) -> int {
  auto value = test_struct{.number1 = 55, .number2 = 33.3f, .message = "Hello", };
  print_members(value);
}

実行結果は以下の通り。 ちゃんと値取れてますね。

0 NUMBER_1 55
1 NUMBER_2 33.3
2 MESSAGE Hello

環境変数から値を読み取って代入する

メンバーの値を取得できるようになったので、今度は逆をやってみます。 フィールド値と同じ名前の環境変数があればそれを読み取って代入する、という関数を書いてみます。

  • 環境変数の取得は std::getenv を使います
  • 文字列から各型への変換は std::from_chars を使います
  • 文字列への変換はstd::from_chars は対応していないので、文字列型の場合は直接代入します

他の型も試したいのでtest_structに文字列メンバーを追加してみます。

#include <iostream>
#include <cstdlib>

#include "glaze/glaze.hpp"

struct test_struct {
  int number1;
  float number2;
  std::string message;
};

template<>
struct glz::meta<test_struct> {
  using T = test_struct;
  static constexpr auto value = glz::object(
    "NUMBER_1", &T::number1,
    "NUMBER_2", &T::number2,
    "MESSAGE", &T::message
  );
};

// print_members関数は前述のコードと同じため省略

namespace internal {

template<typename T, std::size_t IDX>
auto from_env(T& result) {
  if constexpr (IDX < glz::reflect<T>::size) {
    auto const field_name = glz::reflect<T>::keys[IDX];
    if (auto const env = getenv(field_name.data()); env != nullptr) {
      auto& value = [&]() -> decltype(auto) {
        if constexpr (glz::reflectable<T>) {
          // メタ情報がない場合はstd::tupleにしてから取得する
          return glz::get<IDX>(glz::to_tie(result));
        } else {
          // メタ情報がある場合はメタ情報を利用して取得する
          auto& member = glz::get<IDX>(glz::reflect<T>::values);
          return glz::get_member(result, member);
        }
      }();
      // std::from_charsはstd::stringへの変換に対応していないので、std::stringの場合は直接代入する
      if constexpr (std::is_convertible_v<decltype(value), std::string>) {
        value = env;
      } else {
        // TODO: エラーチェックをすること
        std::from_chars(env, env + std::strlen(env), value);
      }
    }
    from_env<T, IDX + 1>(result);
  }
}

}

template<typename T>
auto from_env() {
  T result{};
  internal::from_env<T, 0>(result);
  return result;
}

auto main([[maybe_unused]] int argc, [[maybe_unused]] char* argv[]) -> int {
  auto result = from_env<test_struct>();
  print_members(result);
}

環境変数を設定して実行してみます。

$ NUMBER_1=55 NUMBER_2=33.3 MESSAGE=Hello test_program

実行結果は次の通りです。

0 NUMBER_1 55
1 NUMBER_2 33.3
2 MESSAGE Hello

ちゃんと環境変数から値を読み取って代入できていますね。 glazeのメタ情報だけ定義すれば、あとは関数呼び出し1つで環境変数から構造体に値を読み取って代入できるようになりました。

足りないことはたくさんある

まだまだ足りない点はたくさんありますね。

実装長くなるし、glazeのリフレクションの話からも外れるので、この記事ではここまでとします。

  • エラーハンドリングが全然ない
  • 変換に失敗した場合の処理を書いていない
  • ネストした構造体や配列・ベクターなどのコンテナに対応していない

この実装を拡張しつつ、実用的なコードにしていきたいと思います。 ある程度まとまったら公開したいですね。

まとめ

最低限のやりたいことは達成できたので良しとします。

  • glazeのリフレクション機能を使うと、構造体のメンバー名や型情報を取得できる
  • 任意の構造体のメンバーの名前と値を取得・表示する関数が書けた
  • メタ情報があれば、メンバーの名前を変更してアクセスすることもできた
  • メンバー名と一致する環境変数から値を取得して、構造体に代入する関数が書けた

glazeを使って構造体への読み書きが簡単にできるようになったので、他のフォーマットにも応用できそうです。 何かのパーサーと組み合わせて使うのも面白そうですね。

glazeのリフレクションで構造体のメンバー情報をとってくる

今月も相変わらずglazeをうまく活用することを考えていました。 今回はglazeのリフレクション機能を利用する話です。

※ この記事では2025-10-22にリリースされたglaze v6.0.1を対象としています。

TL;DR

  • glazeはC++23時代の現時点で現実的なリフレクション機能を外部に向けて提供しています
  • ドキュメントは限定的ですが、テストコードを参照することで大抵の機能が利用できます
  • メタ情報を付与している場合と付与していない場合で処理を分ける必要があるのが注意点です

glazeのリフレクション機能

glazeはC++23までの機能を利用して、集成体に対してリフレクションを実現しています。 集成体であればstd::map, std::setどころかコンセプトを利用することで、互換性のあるサードパーティのコンテナがメンバーの場合にも対応しています。

メタ情報を提供しなくても構造体のフィールドへのアクセスを実現していますし、構造体の外でメタ情報を付与することで、「除外するメンバ」「別名定義」なんかを実現できるようになっています。 対象となる構造体に影響を与えないのがありがたいですね。(個人的に大好物)

glazeのリフレクションを利用する

普段はJSON, CSV, TOMLなんかとの相互変換の裏で利用されているリフレクション機能なのですが、一応単独で利用することができます。 あんまりドキュメント化されてはいないのですが、テストコードからどんな機能があるかを理解することができます。

github.com

glz::reflect<T>周りがキモですね。

構造体のメンバー数を取得する

まずは序の口。メンバー数を取得します。

#include <iostream>
#include "glaze/glaze.hpp"

struct test_struct {
  int32_t int1{};
  int64_t int2{};
};

auto main() -> int {
  std::cout << glz::reflect<test_struct>::size << '\n'; // -> 2
}

メンバー名を取得する

メンバー名の取得にはkeysを利用します。

#include <iostream>
#include "glaze/glaze.hpp"

struct test_struct {
  int32_t int1{};
  int64_t int2{};
};

auto main() -> int {
  for (auto const key : glz::reflect<test_struct>::keys) {
    std::cout << key << '\n';  // -> int1, int2
  }
}

keyはstd::string_viewになっています。

keysはstd::array<std::string_view>なので、範囲外にアクセスしないように気をつけないといけません。 やるとしたらこんな感じでしょうか。

// 範囲外アクセスを避ける例
if constexpr (glz::reflect<test_struct>::size > 0) {
  auto first_key = glz::reflect<test_struct>::keys[0];
}

メンバーの名前を変更する

ここから先はメタ情報を利用します。

アクセスできるメンバーの名前を変更することもできます。 「C++予約語と一致している外部データ名」とか「区切り文字が-(ハイフン)のフィールド名」なんかとの連携で便利ですね。

下の例ではint1を"class"、int2を"struct"という名前でアクセスできるようになります。

#include <iostream>
#include "glaze/glaze.hpp"

struct test_struct {
  int32_t int1{};
  int64_t int2{};
};

template<>
struct glz::meta<test_struct> {
  using T = test_struct;
  static constexpr auto value = glz::object(
    "class", &T::int1,
    "struct", &T::int2
  );
};

auto main() -> int {
  for (auto const key : glz::reflect<test_struct>::keys) {
    std::cout << key << '\n';  // -> class, struct
  }
}

メンバーを除外する

メタ情報に定義されていないメンバーは、リフレクション対象から外れます。 下の例ではint2が無視されるようになります。

#include <iostream>
#include "glaze/glaze.hpp"

struct test_struct {
  int32_t int1{};
  int64_t int2{};
};

template<>
struct glz::meta<test_struct> {
  using T = test_struct;
  static constexpr auto value = glz::object(
    "class", &T::int1,
  );
};

auto main() -> int {
  for (auto const key : glz::reflect<test_struct>::keys) {
    std::cout << key << '\n';  // -> class
  }
}

メンバーの値にアクセスする (メタ情報がない場合)

メンバーの情報が取れるようになったら値も取りたいところ。 これは一度メンバーをstd::tupleに変換してからアクセスします。 glazeはメンバーの参照をstd::tupleに変換する関数glz::to_tie()を提供してくれているので、これを利用します。

#include <iostream>
#include "glaze/glaze.hpp"

struct test_struct {
  int32_t int1{};
  int64_t int2{};
};

auto main() -> int {
  auto value = test_struct{};
  auto& int1 = glz::get<0>(glz::to_tie(value));
  int1 = 42;

  std::cout << value.int1 << '\n'; // -> 42
}

メンバーの値にアクセスする (メタ情報がある場合)

上の方法ではメタ情報を考慮せず、「素のメンバーの値をそのまま参照」しています。 メタ情報がある場合には、これを考慮するために以下のようにちょっと面倒なことをします。

#include <iostream>
#include "glaze/glaze.hpp"

struct test_struct {
  int32_t int1{};
  int64_t int2{};
};

template<>
struct glz::meta<test_struct> {
  using T = test_struct;
  static constexpr auto value = glz::object(
    "int2", &T::int2
  );
};

auto main() -> int {
  auto value = test_struct{};
  auto& element = glz::get<0>(glz::reflect<test_struct>::values);
  auto& int2 = glz::get_member(value, element);

  int2 = 42;

  std::cout << value.int2 << '\n'; // -> 42
}

メタ情報ではint2だけの情報を定義しているので、メンバーとしてはint2だけを参照したいですよね。 これを実現するために、glz::reflect<test_struct>::valuesでtest_structの各メンバーへのポインタを取得し、これとtest_structのインスタンスglz::get_member()に渡すことで、メンバーへの参照を取得しています。

あとは上と一緒ですね。

ちなみにメタ情報は名前の変更が不要な場合には、メンバーのポインタだけを指定することもできます。

template<>
struct glz::meta<test_struct> {
  using T = test_struct;
  static constexpr auto value = glz::object(
    &T::int2
  );
};

どっちの場合でも動くようにする

うまくいったように見えますが、glz::reflect<T>::values はメタ情報が定義されていないとコンパイルエラーになってしまいます。

メタ情報がある場合にはそっちを参照し、メタ情報がない場合は素直にメンバーのリストから取得する

これを実現するためには、glz::reflectable を使ってメタ情報の有無でif constexprすることが考えられます。

glz::reflectable<T> は次のような値になります。 - Tにメタ情報が定義されていない場合: true - Tにメタ情報が定義されている場合: false

#include <iostream>
#include "glaze/glaze.hpp"

struct test_struct {
  int32_t int1{};
  int64_t int2{};
};

auto main() -> int {
  auto value = test_struct{};

  auto& int1 = []<class T>(T& value) -> decltype(auto) {
      if constexpr (glz::reflectable<T>) {
        // メタ情報がない場合はstd::tupleにしてから取得する
        return glz::get<0>(glz::to_tie(value));
      } else {
        // メタ情報がある場合はメタ情報を利用して取得する
        auto& member = glz::get<0>(glz::reflect<T>::values);
        return glz::get_member(value, member);
      }
  }(value);

  int1 = 42;

  std::cout << value.int1 << '\n'; // -> 42
}

test_structにメタ情報を定義すると、if constexprのelse節が採用されるのでメタ情報を意識した処理になります。

色々苦労した点はあるのですが、説明をすると長くなるのと、筆者の能力を越えているため正しい言葉で説明できるか微妙なので箇条書きにだけしておきます。

  • if constexprを利用するためにテンプレート関数にしています
  • 戻り値を参照値として推論してもらうために decltype(auto) を戻り値の型にしています

まとめ

C++23の範囲でかなり柔軟なリフレクションを実現しているglazeのリフレクション機能の紹介をしてみました。 std::variantへの対応や、異なる構造体型のインスタンス間での一括コピーなど、面白い機能が色々あるのですが、今回はここまでにしておきます。

「これを使って何ができるか?」についてはちまちまコードを書いて検証しているので別の記事で紹介できればと思っています。

glazeはTOMLも簡易的ながら読み書きできる

はじめに

普段から使っているライブラリのドキュメントになっていない便利な機能を発見すると嬉しくなります。 今日は普段遣いしているglazeというC++のライブラリでそういう嬉しい体験をしたので紹介します。

TL:DR

  • glazeにはドキュメントはほとんどないが、TOMLの読み書きできる機能があります
  • ただしTOMLの仕様を完全にサポートしてはいません
  • JSONやBEVEのようにメタ情報の付与もまだできません
  • glazeが別フォーマットへの対応が容易にできるように設計されていることを示す実例にもなってます

glazeの紹介

C++使いの皆さん、glazeというライブラリをご存知でしょうか? glazeはC++JSONを安定して高速に読み書きできるライブラリです。

ばりばりにSIMD命令で高速化したsimdjsonには及びませんが、yyjsonとだいたい同じぐらいの性能を、あまりSIMD命令を利用しないで実現している点が特徴です。

詳しくはjson_performanceを見てください。

そんな高速さを持ちながら、JSON文字列とC++の構造体を直接マッピングでき、メタ情報の提供によって細かく制御できる点が便利です。 個人的には、ここ数年、必須というぐらいに使いまくっているライブラリです。

一方でC++23の機能を積極的に使っているので、組織の方針で使えないケースも多いとは思います。 はやくC++23が普及してほしいものです。

glazeが(公式に)対応しているフォーマット

公式ドキュメントに記載がある通り、glazeは以下のフォーマットに対応しています。

  • JSON
  • CSV
  • BEVE (glaze独自のバイナリフォーマット
  • Stencil/Mustache
  • EETF (Erlang External Term Format))

とはいえ、EETFはErlangのランタイムが必要ですし、Stencil/Mustacheはテンプレートエンジンとしての機能提供なので、実質的には上の3つになるかなと思います。

実はTOMLも読み書きできる

公式なドキュメントには記載がありませんが、glazeは2025年に入ってからTOMLの読み書きもできるようになりました。

  • 書き込みは2025-02-15リリースのv4.4.2
  • 読み込みは2025-06-10リリースのv5.4.1

他のフォーマットに漏れず、C++の構造体と直接マッピングできるようになっています。

#include <iostream>

#include "glaze/glaze.hpp"
#include "glaze/toml.hpp" // TOML対応のためには必要です

struct my_struct {
  int integer{};
  double real{};
  std::string string{};
};

int main() {
    auto toml_str = std::string{R"(
integer = 387
real = 3.14159
string = "Hello World"
)"};

  my_struct toml_val;
  auto toml_result = glz::read_toml(toml_val, toml_str);

  if (toml_result) {
    std::cout << "TOML parse error: " << glz::format_error(toml_result) << "\n";
  } else {
    std::cout << toml_val.integer << " " << toml_val.real << "\n";
  }
}

とはいえ、TOMLは柔軟な書き方ができるフォーマットなので、glazeがサポートしているのはTOMLの仕様の一部に過ぎません。 例えば、配列やコメントにはまだ対応していません。

#include <iostream>

#include "glaze/glaze.hpp"
#include "glaze/toml.hpp" // TOML対応のためには必要です

struct my_struct {
  int integer{};
  double real{};
  std::string string{};
  std::vector<int> array{};
};

int main() {
    auto toml_str = std::string{R"(
integer = 387
real = 3.14159
string = "Hello World"
array = [
  1,
  2,
  3
]
)"};

  my_struct toml_val;
  auto toml_result = glz::read_toml(toml_val, toml_str);

  // 常に失敗します
  if (toml_result) {
    std::cout << "TOML parse error: " << glz::format_error(toml_result) << "\n";
  } else {
    std::cout << toml_val.integer << " " << toml_val.real << "\n";
  }
}

実行結果:

TOML parse error: parse_number_failure

ちょっと前から実装されている書き込みについては、配列についても対応しているようです。

#include <iostream>

#include "glaze/glaze.hpp"
#include "glaze/toml.hpp" // TOML対応のためには必要です

struct my_struct {
  int integer{};
  double real{};
  std::string string{};
  std::vector<int> array{};
};

int main() {
  my_struct value = {387, 3.14159, "Hello World", {1, 2, 3}};
  auto toml_str = glz::write_toml(value).value_or("error");
  std::cout << toml_str << "\n";
}

実行結果:

integer = 387
real = 3.14159
string = "Hello World"
array = [1, 2, 3]

ちょっと中途半端な実装になっているTOML対応ですが、簡単な設定ファイル用途であればJSONパースのついでに使えるので、便利に使っています。 コメントにだけは対応してほしかったりしますが…今後の実装に期待ですね。

TOML対応はglazeの柔軟性の証明

個人のひいき目かもしれませんが、TOML対応自体は、glazeの設計が柔軟であることの証明にもなっていると思っています。

TOML対応は以下のissueから始まっています。 作者のStephen Berryさんがさらっと実装してくれたようですね。

https://github.com/stephenberry/glaze/issues/1594

issueのコメントにもあるとおり「あとはユーザーが必要に応じて実装してね」という方針で、実装は簡易でglazeのコアの機能を利用しているだけです。

たとえば読み込み処理は1000行以下でこれでリフレクションに対応してくれるのは助かるなぁと思っています。

https://github.com/stephenberry/glaze/blob/v5.7.2/include/glaze/toml/read.hpp

今後の発展に期待がもてるライブラリだなとあらためて思う次第でした。

PATH_MAXを使うと環境によってコンパイルエラーになる

PATH_MAXを使いたいだけなのにエラーになる

はじめに

ファイルシステムのパスの最大値を取得する」方法として、macOSLinuxなら定数PATH_MAXを使う方法があります。 MSVCだったらMAX_PATHですね。

しかし、これを使うとエラーが出ることがあります。 これで結構はまってしまったので原因と解決策をまとめておきます。

ことの始まり

最近ちょいちょい色々なところで使われるようになったplutovgの1.3.0リリースを使おうとしたときのことです。 いつも使っている環境では何なく使えたのですが、別途使っているサーバー環境でビルドすると以下のようなエラーが出ました。

PATH_MAX’ undeclared (first use in this function)

発生場所はこちら。

https://github.com/sammycage/plutovg/blob/v1.3.0/source/plutovg-font.c#L1009

同じgccのバージョンを利用しているのに、こんなことが起きるのか?と不思議に思いながら調査を始めた次第です。

PATH_MAXには問題があるので注意

出鼻を挫くようですが、PATH_MAXの値の正しさには問題があります。 最近のLinuxではファイルシステムの実装によって最大パス長が動的に変更することができるため、PATH_MAXが常に正しいとは限らないのです。

https://www.reddit.com/r/C_Programming/comments/ke1hf6/path_max_any_downsides/?show=original

これの代替案はpathconffpathconfを使うことなのですが、これは後程話すことにして、今は

Linux環境ではPATH_MAXはあまり信用できない

ということだけ覚えておいてください。

PATH_MAXを使いたい場合のエラー

本題に戻ります。

PATH_MAXを使いたい場合は<limits.h>をインクルードする必要がありますが、includeしても以下のようなエラーが出ることがあります。 ちなみにmacOSでのApple Clangではこの問題は発生しません。

’PATH_MAX’ undeclared (first use in this function)

ちなみにPATH_MAXは<limits.h>で定義されていることになっていますが、実際にはさらにその先のヘッダーで定義されていることが多いです。 私の環境ではlinux/limits.hで定義されていました。(GPL-2.0のコードなので掲載はしません。)

  • linux/limits.hをincludeしているのがbits/local_lim.h
  • bits/local_lim.hをincludeしているのがlimits.h となっています。 そしてlimits.hbits/local_lim.hをincludeする条件は__USE_POSIXが定義されていることになっています。

つまり__USE_POSIXが定義されていればPATH_MAXが使えるようになるわけです。

問題を発生させる簡単なコード

毎回plutosvgをコンパイルするわけにもいかないので、再現する簡単なコードを用意しました。

#include <stdio.h>

#include <limits.h>

int main() {
  printf("PATH : %d\n", PATH_MAX);
}

コンパイルしてみましょう。

gcc path_max.c

問題なくコンパイルできてしまいます。

では、-std=c11オプションを付けてコンパイルしてみましょう。

gcc -std=c11 path_max.c

でましたエラーメッセージ。 これが今回の事象の再現になります。

path_max.c: In function ‘main’:
path_max.c:6:25: error: 'PATH_MAX' undeclared (first use in this function)
    6 |   printf("PATH : %d\n", PATH_MAX);
      |                         ^~~~~~~~
path_max.c:6:25: note: each undeclared identifier is reported only once for each function it appears in

解決策

強引な解決策

強引に__USE_POSIXを定義してしまう方法があります。 他の部分で色々問題おきそうですが、ひとまずは動かすことはできます。

#include <stdio.h>

#define __USE_POSIX
#include <limits.h>

int main() {
  printf("PATH : %d\n", PATH_MAX);
}

もうすこしまともな解決策

-std=gnu11オプションを付けてコンパイルすると、安全に__USE_POSIXが定義されて、PATH_MAXが使えるようになります。

gcc -std=gnu11 path_max.c

根本的な解決策

冒頭でも描いた通り、PATH_MAXはあまり信用できない変数です。 これの代替として、pathconffpathconfを使う方法があります。

ここではpathconfを使う例を示します。

#include <stdio.h>

#include <unistd.h>

int main() {
  int path_max = pathconf(".", _PC_PATH_MAX);
  printf("pathconf : %d\n", path_max);
}

定数ではなくなってしまいますが、これが一番安全な方法だと思います。

まとめ

PATH_MAX一つで結構はまってしまいました。

gccを素で使っているとGNU 拡張が有効になっているので気がつきませんが、-std=c11と明示的に使用言語のバージョンを指定するとgnu拡張が無効になるので__USE_POSIXが定義されなくなり、PATH_MAXが定義されなくなる…というのが背景でした。

PATH_MAXは言語標準の定数ではないので、macOSLinuxでたまたま同じ名前で提供されているからといって、あまり使わない方が良さそうですね。

ちゃんとやるなら、以下のような使い分けをしたほうが良さそうです。

プラットフォーム 推奨
Windows MAX_PATH
macOS PATH_MAX
Linux pathconfで該当パスを指定して_PC_PATH_MAX

Quillでstd::chronoを書式化しようとしてMissing Codecエラーになる

C++20以降の世界ではstd::chronoを使うのがすっかりあたり前になりつつありますね。ありがたや。

私もstd::chronoの恩恵に預かっていたのですが

std::chronoが提供する型を標準出力やログに出力したい

という用途がでてきて少し嵌ったのでメモしておきます。

fmtlibでは問題なく動く

std::printと同様にfmt::printであればfmt/chrono.hをincludeすることで、{:<format>}と任意の形式で日付・時刻を表示することができます。

#include <chrono>

#include "fmt/chrono.h"

int main() {
  auto now = std::chrono::system_clock::now();
  fmt::print("Time: {:%H:%M:%S}\n", now);
}

実行結果は次のようになります。

Time: 20:10:39.467474536

std::print(C++23以降)でも問題なく動く

C++23に対応していないといけませんが、std::printでも問題なくstd::chronoが使えます。

#include <print>
#include <chrono>

int main() {
  auto now = std::chrono::system_clock::now();
  std::print("Time: {:%H:%M:%S}\n", now);
}

Quillを使う場合に問題が発生してしまう

ところがQuillでは問題がおきてしまいます。

QuillとはC++ロギングライブラリです。
すごく高速に動き、さまざまな出力先をサポートしていて、fmtlibサポートによるフォーマットに対応しているライブラリで、C++でログを出力したい場合に便利です。

ここでのQuill10.0.1をvcpkgでインストールして使っています。

ソースコードは以下の通り。

#include <chrono>

#include "quill/Backend.h"
#include "quill/Frontend.h"
#include "quill/LogMacros.h"
#include "quill/sinks/ConsoleSink.h"

int main() {
  quill::Backend::start();

  auto logger = quill::Frontend::create_or_get_logger(
    "root", quill::Frontend::create_or_get_sink<quill::ConsoleSink>("root_sink"));

  auto now = std::chrono::system_clock::now();
  LOG_INFO(logger, "Time: {:%H:%M:%S}\n", now);
}

悲しい。 けどちゃんとエラーメッセージが出てくれるのありがたい。

blog-2025-08-25/03_quill_error.cpp:13:3:   required from here
   13 |   LOG_INFO(logger, "Time: {:%H:%M:%S}\n", now);
      |   ^
blog-2025-08-25/build/vcpkg_installed/x64-linux/include/quill/core/Codec.h:64:5: エラー: static assertion failed: 
+------------------------------------------------------------------------------+
|                       Missing Codec for Type 'Arg'                           |
+------------------------------------------------------------------------------+

Error: A codec for the specified type 'Arg' is not available.

Possible solutions:
1. If Arg is an STL type:
   - Ensure you have included the necessary headers for the specific STL type you are using from the quill/std folder.

2. If Arg is a user-defined type:
   - Use either 'DeferredFormatCodec' or 'DirectFormatCodec'.
   - Define a custom Codec for your type.
   - Consider converting the value to a string before logging.

Note: The specific type of 'Arg' can be found in the compiler error message.
      Look for the instantiation of 'codec_not_found_for_type<Arg>' in the error output.
      The compiler should indicate what type 'Arg' represents in this instance.

For more information see https://quillcpp.readthedocs.io/en/latest/cheat_sheet.html

丁寧なエラーメッセージの通り「std::chrono::time_pointに対応するquill::Codecがない」のが原因ですね。

quill::Codecの定義は以下の通り。

/** typename = void for specializations with enable_if **/
template <typename Arg, typename = void>
struct Codec
{
  /***/
  QUILL_NODISCARD QUILL_ATTRIBUTE_HOT static size_t compute_encoded_size(
    QUILL_MAYBE_UNUSED detail::SizeCacheVector& conditional_arg_size_cache, QUILL_MAYBE_UNUSED Arg const& arg) noexcept
  {

専用のヘッダーをインクルードすると解決

ちゃんと公式ドキュメントに解決策がのってました。 quillcpp.readthedocs.io

タイトルにSTLって書いてありますが、std::chrono関連も対応するヘッダー(quill/std/Chrono.h)を明示的にインクルードすれば良いとのこと。

- #include "quill/sinks/ConsoleSink.h"
+ #include "quill/sinks/ConsoleSink.h"
+ #include "quill/std/Chrono.h"

念のため適用したコードは以下の通り。

#include <chrono>

#include "quill/Backend.h"
#include "quill/Frontend.h"
#include "quill/LogMacros.h"
#include "quill/sinks/ConsoleSink.h"
#include "quill/std/Chrono.h"

int main() {
  quill::Backend::start();

  auto logger = quill::Frontend::create_or_get_logger(
    "root", quill::Frontend::create_or_get_sink<quill::ConsoleSink>("root_sink"));

  auto now = std::chrono::system_clock::now();
  LOG_INFO(logger, "Time: {:%H:%M:%S}\n", now);
}

この1行追加だけで、うまく動くようになりました。
公式ドキュメント読むの大事ですね。

12:02:35.870619739 [3609912] 04_quill.cpp:14              LOG_INFO      root         Time: 03:02:35.870619654

ちなみにquill/std/Chrono.hにはquill::Codecの特殊化が定義されています。

template <template <typename...> class ChronoType, typename TimeSpec, typename DurationType>
struct Codec<ChronoType<TimeSpec, DurationType>,
             std::enable_if_t<std::disjunction_v<std::is_same<ChronoType<TimeSpec, DurationType>, std::chrono::time_point<TimeSpec, DurationType>>,
                                                 std::is_same<ChronoType<TimeSpec, DurationType>, std::chrono::duration<TimeSpec, DurationType>>>>>
  : DeferredFormatCodec<ChronoType<TimeSpec, DurationType>>
{
};

DeferredFormatCodecを継承しているので、fmtlibのフォーマット指定子がそのまま使えるようになっています。
DeferredFormatCodecでは文字列へのフォーマットは別スレッドのバックエンドでやってくれるのでメインスレッドの負荷はほとんどありません。ありがたい。

quill/std以下のヘッダー一覧

quill/std/Chrono.h以外にもquill/std以下にはたくさんのヘッダーがあって、色々な型に対応しています。

quill/include/quill/std at master · odygrd/quill · GitHub

ヘッダー 対応する型
Array.h std::array, charの配列
Chrono.h std::chrono::time_point, std::chrono::duration
Deque.h std::deque
FilesystemPath.h std::filesystem::path
ForwardList.h std::forward_list
List.h std::list
Map.h std::map, std::multimap
Optional.h std::optional
Pair.h std::pair
Set.h std::set, std::multiset
Tuple.h std::tuple
UnorderedMap.h std::unordered_map, std::unordered_multimap
UnorderedSet.h std::unordered_set, unordered_multiset
Vector.h std::vector
WideString.h std::wstring, wchar_tの配列

ちなみにstd::unordered_mapだとこんな感じになります。

#include <unordered_map>

#include "quill/Backend.h"
#include "quill/Frontend.h"
#include "quill/LogMacros.h"
#include "quill/sinks/ConsoleSink.h"
#include "quill/std/UnorderedMap.h"

int main() {
  quill::Backend::start();

  auto logger = quill::Frontend::create_or_get_logger(
    "root", quill::Frontend::create_or_get_sink<quill::ConsoleSink>("root_sink"));

  auto value = std::unordered_map<int, std::string>{{1, "one"}, {2, "two"}, {3, "three"}};

  LOG_INFO(logger, "unordered_map: {}\n", value);
}

実行結果は以下の通り。 ちゃんと中身が表示されてますね。便利。

11:58:29.260206994 [3592695] 05_unordered_map.cpp:20    LOG_INFO      root         unordered_map: {1: "one", 2: "two", 3: "three"}

まとめ

Missing Codec for Type 'Arg'(std::chrono::time_point)というコンパイルエラーは、対応するquill::Codecが未定義なのが原因です。
対応するquill::Codecを定義しているquill/std/Chrono.hをインクルードすることで解決できます。

std::chrono以外でも必要な型に対応するquill/std/<Header>.hを追加でincludeすることで、追加実装をすることなくQuillでログ出力できるようになります。

補足

ソースコードとvcpkg.json, CMakeLists.txtなどについては以下に登録してあります。 github.com

C++でURLを正規化する、リダイレクト先のURLを取得する

前提

スクレイピングC++で実装していると、URLの正規化がしたくなることがあります。

相対的なPATHだけがhref属性に指定されている時に、https://www.example.com/a/b/../c/d/index.htmlみたいなURLをログに出すより、https://www.example.com/a/c/d/index.htmlと表示したいよね、みたいな要望です。

今までは自前で実装したり、std::filesystemの力を借りたりとかしていたのですが、できればちゃんとWHATWG仕様に準拠した正規化をしたいところ。 これを実現することができるライブラリがあったので紹介です。

結論

ada-url/adaを使いましょう。

ただadaではリダイレクト先のURLまでは分かりません。 その用途ではlibcurlかcprを利用するのがおすすめです。

  • libcurlならお手軽に取得できます
  • cprはlibcurlに追加でインストールが必要ですが、少し簡潔に記載できます

ada-url/adaで正規化する

ada-url/adaはWHATWG仕様に準拠したURLのパースを高速に実現するライブラリです。 国際化ドメイン名のためにpunycodeへの対応もしてくれていて優秀です。 github.com

Apache-2.0かMITライセンスで利用できるので、たいていの場合問題なく利用できるはず。

明確な正規化のメソッドはないのですが、パース結果が正規化したURLになっています。

#include <iostream>

#include "ada.h"

int main() {
  auto const raw_url = "HttPs://Example.com:80/a/../b/./c?x=1#Fragment";
  auto const url = ada::parse(raw_url);
  if (not url) {
    std::cerr << "invalid url\n";
    return 1;
  }
  std::cout << url->get_href() << '\n'; // -> http://example.com/b/c?x=1#Fragment
  return 0;
}

高速に動作するし、仕様準拠だしで、安心して使えます。おすすめ。

リダイレクト先のURLを取得する

リンクのURLがリダイレクトされて最終的にまったく別のURLになっている場合があります。 これもできれば最終的なURLを取得したいところ。

この用途では通信をしないada-url/adaは利用できないので、http/httpsクライアントライブラリを利用します。

libcurlを利用する方法

HTTP関連なら何でもできる、安定のlibcurl。

#include <iostream>

#include "curl/curl.h"

int main() {
  auto curl = curl_easy_init();

  curl_easy_setopt(curl, CURLOPT_URL, "https://yahoo.com/");
  // リダクレクト先まで辿っていく 
  curl_easy_setopt(curl, CURLOPT_FOLLOWLOCATION, 1L);
  // BODYの受信はしない
  curl_easy_setopt(curl, CURLOPT_NOBODY, 1L);

  // リクエスト送信
  curl_easy_perform(curl);

  // リダクレクト先のURLを取得
  char* final_url = nullptr;
  curl_easy_getinfo(curl, CURLINFO_EFFECTIVE_URL, &final_url);
  if (final_url) {
    std::cout << final_url << '\n'; // -> https://www.yahoo.com/
  }
  curl_easy_cleanup(curl);
  return 0;
}

cprを利用する方法

C++っぽく書きたいのならばcprを使うとシンプルに書けます。
CURLOPT_NOBODYを手軽に指定できるPrepareHead()を利用しています。

#include <iostream>

#include "cpr/cpr.h"

int main() {
  auto session = cpr::Session{};
  session.SetUrl(cpr::Url{"http://yahoo.com"});
  session.PrepareHead();
  auto const res = session.Head();

  std::cout << res.url.str() << '\n'; // -> https://www.yahoo.com/

  return 0;
}

cpp-httplibでもできるはずなのですが…

cpp-httplibでもここまで手軽ではないものの、status 301, 302をたどっていけば取れるはず。 私の環境では例でつかっているhttp://yahoo.comへのアクセスが不安定だったので断念しました。

何か変な挙動なので、どこかで追試してみたい。

Rust, C, C++の生成バイナリのファイルサイズ比較

「Rustが生成するバイナリのサイズがC言語のそれより無茶苦茶大きい」みたいなXの投稿を見て、「さすがにそれはアンフェアだろう」と思ったので追試してみました。

利用したコンパイラは以下の通りです。

  • rustc 1.88.0
  • gcc 15.1.1

実行環境はFedora Linux 42になります。

言いたいこと

RustとたいていのC,C++ではデバッグ情報についての考え方が真逆です。

ちゃんと条件そろえた上で、stripしてシンボル情報も削除した上で比較しないと正しい比較にならないです。

今回の比較では次の条件での比較をすることとしました。

  1. デバッグ情報を含まない
  2. 最適化はRustのリリースビルドにあわせて「できるだけ最適化できる」する
  3. stripでシンボル情報を削除する

Rust

fn main() {
  println!("Hello, World!");
}

デフォルトのバイナリ生成ではデバッグ用途のバイナリが生成されるので、cargoのreleaseビルドに近いオプションを明示的に設定してコンパイルします。

> rustc -C debuginfo=0 -C opt-level=3 -C panic=abort main.rs -o main_rs
> wc -c main_rs
3665680 main_rs
> strip main_rs
> wc -c main_rs
349344 main_rs

3.5MBのバイナリがstripで341.1KBになりました。 大きくはありますが、stripすると1/10になってまあ現実的な感じですね。

C言語

#include <stdio.h>

int main(int argc, char* argv[]) {
  printf("Hello, World\n");
}

デフォルトだと最適化が指定されていないので、rustcに合わせて-O3を指定してみます。

> gcc -std=c11 main.c -O3 -o main_c
> wc -c main_c
12520
> strip main_c
> wc -c main_c
10880

10.6KBになりました。 流石に小さい。

printf関数は引数が固定文字列だとputs関数に置き換わるので「そりゃ小さくなるわな」という感想です。

C++11

ここからは蛇足になりますが、RustとC言語の提供機能の差を考えるとC++も比較対象にした方がよいかなと思ったのでやってみます。

#include <iostream>

int main(int argc, char* argv[]) {
  std::cout << "Hello, World!\n";
}

C言語と同じく-O3を指定します。

> g++ -std=c++11 main.cpp -O3 -o main_cpp
> wc -c main_cp
12712 main_cpp
> strip main_cpp
> wc -c main_cpp
10880 main_cpp

iostreamを使っているのにC言語と同じサイズになりました。 昔はiostream使うだけでバイナリサイズが無茶苦茶大きくなっていたような…コンパイラの最適化技法の進化の賜物でしょうか。

C++23

蛇足の蛇足ですが、C++23から使えるstd::printを使ってみました。

#include <print>

int main(int argc, char* argv[]) {
  std::print("Hello, World!\n");
}

ここでコンパイル時間が一気に20倍になりました。これは期待?できそうです。

> g++ -std=c++23 main23.cpp -O3 -o main_cpp23
> wc -c main_cpp23
135480 main_cpp23
> strip main_cpp23
> wc -c main_cpp23
121968 main_cpp23

119.1KB。iostream版の一気に10倍になりました。 Rustの1/3ぐらいのサイズなので十分小さくはありますが。

std::printは機能豊富な関数なので普通にやればこんなものな気がします。 今後の実装の進化で、改善されるんじゃないかなと思っています。

結論

コンパイルの条件を揃えてコンパイル後にstripしてから比較する」という条件の元での比較では次のようになりました。

言語 サイズ(byte)
Rust 349,344
C言語 10,880
C++11 10,880
C++23 121,968

当初のXの投稿では3MBのバイナリでC言語と200倍以上の差というなかなか衝撃的な数字だったのですが、条件揃えれば35倍ぐらいに納まりました。 C++23との比較だと3倍弱ぐらい。

まだ大きな差ではありますが、とんでもない差ではないよね、というネタ投稿をまじめに検証してみた話でした。 個人的にはC++11のiostreamを使ったバイナリのサイズがC言語と同じになるのが驚きでした。

/.l2s/.l2s.perlファイルがないせいでvcpkgのOpenSSLのビルドが失敗する

termuxのproot環境でvcpkgのOpenSSLのビルドをしようとすると、以下のようなエラーが発生することがあります。

/.l2s/.l2s.perl: No such file or directory

むっちゃ途方にくれたのですが、回避策をみつけて無事にOpenSSLのビルドに成��したので、おそらく世界に数人いるであろう同じ問題に直面している人のために、メモを共有します。

回避策

「とにかくOpenSSLがビルドしたいだけなんだ」という人は、

export PERL=/usr/bin/perl

とした上で、vcpkgのOpenSSLをビルドすればOKです。 ちょっと時間はかかりますが、OpenSSLのビルドが成功します。

bashの場合なので他のシェルでは適宜読み替えてください

原因

proot環境で動かしていることで、link2symlinkの挙動でperlコマンドのファイルが/usr/bin/perlではなく、/.l2s/.l2s.perl;682bda2c0001.0002というパスで参照されてしまうのですが、これをOepnSSLのConfigureが正しく解釈できないのが原因です。

実行するPerlコマンドを決定している箇所はOpenSSLのConfigureスクリプトの以下の部分です。

    PERL        => env('PERL') || ($^O ne "VMS" ? $^X : "perl"),

https://github.com/openssl/openssl/blob/openssl-3.5.0/Configure#L774

PERL環境変数が設定されていれば問題なく動作しますが、設定されていない場合は、$^XPerlの実行ファイルのパス)を参照しようとします。

これがproot環境では次のようになります。

> perl -e 'print("$^X");'
/.l2s/.l2s.perl;682bda2c0001.0002

Configureではこれをこのままsystem()に渡して実行しようとするので、";"で分割されてしまい、/l2s/.l2s.perlというファイルが存在しないというエラーが発生します。

my $perlcmd = (quotify("maybeshell", $config{PERL}))[0];
my $cmd = "$perlcmd $configdata_outname";
#print STDERR "DEBUG[run_dofile]: \$cmd = $cmd\n";
system($cmd);

https://github.com/openssl/openssl/blob/openssl-3.5.0/Configure#L3011-L3014

検索でも情報でてこないので解決にだいぶ時間かかってしまいました。

まとめ

proot環境でOpenSSLをビルドする場合は、PERL環境変数を設定してからビルドを行う必要があります。

いやはや、まさかこんなことではまるとは思っていませんでした。 類似の事象に遭遇した人に役に立つことを祈って。

「CSS完全設計ガイド」はとても丁寧に作られた良本でした

去年発売された本ですが、今さら読みました。 gihyo.jp

読んだ動機

私はフロントエンド、特にデザイン周りはからっきしです。
フロントエンドやるときはJavascriptのチューニングばっかりなので、HTML+CSSは最低限知っている程度のレベルです。

WebスクレイピングをしているとCSSクラスの指定が結構複雑で「何かベストプラクティスがあるんだろうなぁ」と思っていまいた。
そこらへんの知識をちょっと除き見して、あわよくばスクレイピングで利用したかったのです。

感想

HTML + CSSをかじったことのある程度の私にはちょうど良い本でした。
とても丁寧に作られている本だと思ったので、本来の対象読者であるHTML+CSS書く人以外でもお勧めだと思います。

500ページ以上ある大作ですが、HTML+CSSのコードが大量に掲載されているためなので、そんなに重くなく読み進めることができます。
かといって「HTML+CSSのコードが貼りまくられてページ数水増ししている本」にならないように、「意味が伝わる必要最低限のコード」を筆者が気を配って用意しているのを強く感じます。
この方針が破綻なく最後まで続くので、ほんと凄いなぁと思いました。

おおざっぱな中身の紹介

導入部分ではCSS設計の重要性を話した上で、Atomic Designの紹介を経てOOCSS,SMACSS, BEM, PRECSSの紹介と続きます。

その後、BEM, PRECSSでの具体的なコンポーネントの設計・実装例に移ります。
この話の流れも自然ですっと理解できました。

  • レイアウトと装飾のクラスは分けて指定する
  • Modifier, Mixinのクラス名はスコープを明示する

とかは確かにスクレイピングで見るHTMLで見かける内容だなぁと追体験できました。

設計を原理主義として話すだけではなく、冗長になる部分や設計の境界を迷う部分についても解説されているのも納得感が高かったです。

後半部分でコンポーネントを組み合わせて複雑なレイアウトを実現する際に、CSSの再利用を多用していく部分なんかは自分で書けたらなかなか気持ち良さそうに思いました。
設計の重要性を感じとれる瞬間ですね。

最後にツールの紹介もされているので、CSS設計についてなんとなく全体を俯瞰できた感じがします。
恥ずかしながら、CSS MQ Packerを安易に適用すると駄目なこととか、スタイルガイドという言葉自体初めて知りました。

まとめ

最初にも書きましたが、とても丁寧に作られた本だなぁと思いました。

普通のプログラミングとは異なりかなり制約がきつい世界なのですが、様々な人の知見で設計手法が作り出され、再利用性が確立されているのを知れただけでも良かったです。

文字列から浮動小数点数に変換する、なるべく速く

TL;DR

文字列から浮動小数点数に変換するならfastfloat使いましょう。
私が試せる環境で比較する限り、とても速いです。

細かいことが気になります

C++でちょっとしたプログラムを書くときにいつも気になるのが

「文字列データから指定データ型への変換処理をどうやって効率的に書くか」

です。私だけかもしれませんが。
特に悩んでしまうのが「文字列→浮動小数点」です。

  • std::scanf, std::stringstreamを使うものは大抵すごく遅い
  • std::strtodstd::stodはstd::stringへの変換が入るので避けたい
  • std::from_charsは(libstdc++が)浮動小数点型に対応していない
  • boost::sprit::qiが何故か速いのだけれどこのためにboost::sprit使うのは重い

と色々制約が多いのです。どうにかならないものか。

fast_floatの紹介

…と思っていたら見付けたのがsimdjsonの作者であるlemireさんが開発しているfast_floatです。 github.com

もともとはfast_double_parserとして開発されいて、こちらはGo言語に移植されてstrconv.ParseFloat として標準化されているようですね。そんな経緯があるとは知らなんだ。
Microsoft LightGBMでも利用されているようで。
GitHub - lemire/fast_double_parser: Fast function to parse strings into double (binary64) floating-point values, enforces the RFC 7159 (JSON standard) grammar: 4x faster than strtod

このfast_double_parserをstd::from_charsのAPIに寄せて書き直したものがfast_floatになります。
こちらの実装はApache ArrowやYandex ClickHouseに利用されているようですね。モテモテ。
さらに最近lemire/fast_floatからfastfloat/fast_floatに移動して、少しずつですが更に高速化しています。

ベンチマークのプログラムの準備

「どのくらい速いのか?」については、lemireさんがベンチマークを公開しています。
github.com

ベンチマークを実行すると以下のライブラリのランダムな浮動小数点数表記の文字列のdoubleへの変換速度を計測してくれます。

このベンチマークを実行するだけだと芸がないので、以下のベンチマークも追加しています。

  • boost::spirit:qi
  • fast_float (fixed) 指数表記以外の10進数表記に対応したロジック

GitHub - toge/simple_fastfloat_benchmark

ベンチマークのコードは、コードを内包したりCMakeのFetchContent機能を使っているので、ライブラリを別途インストールする必要ありません。
git, cmake, gccがあれば、お手軽に試せます。

git clone https://github.com/toge/simple_fastfloat_benchmark
cd simple_float_benchmark
export CXXFLAGS="-O3 -march=native"
cmake -B build .
cmake --build build --config Release
build/benchmarks/benchmark

ベンチマークの結果

fastfloat (fake)っていうのは「パースだけして数値計算しない」ロジックなので理論値みたいなものですかね。

AMD Ryzen 1700(自作PC) gcc 10.2.1

fastfloat (fake) : 1595.02 MB/s (+/- 3.7 %) 76.02 Mfloat/s  9.14 i/B  201.00 i/f (+/- 0.0 %) 0.00 bm/B 0.00 bm/f (+/- 76.1 %)  1.99 c/B  43.75 c/f (+/- 3.3 %) 4.59 i/c 3.33 GHz 
netlib           :  215.83 MB/s (+/- 2.5 %) 10.29 Mfloat/s 32.26 i/B  709.76 i/f (+/- 0.0 %) 0.19 bm/B 4.14 bm/f (+/- 0.1 %)  14.70 c/B 323.33 c/f (+/- 2.2 %) 2.20 i/c 3.33 GHz 
doubleconversion :  130.89 MB/s (+/- 3.2 %)  6.24 Mfloat/s 56.35 i/B 1239.78 i/f (+/- 0.0 %) 0.12 bm/B 2.75 bm/f (+/- 2.0 %)  24.26 c/B 533.80 c/f (+/- 3.0 %) 2.32 i/c 3.33 GHz 
strtod           :  141.70 MB/s (+/- 1.9 %)  6.75 Mfloat/s 51.91 i/B 1142.06 i/f (+/- 0.0 %) 0.13 bm/B 2.79 bm/f (+/- 0.3 %)  22.45 c/B 493.82 c/f (+/- 1.5 %) 2.31 i/c 3.34 GHz 
abseil           :  342.08 MB/s (+/- 5.3 %) 16.30 Mfloat/s 30.11 i/B  662.51 i/f (+/- 0.0 %) 0.02 bm/B 0.50 bm/f (+/- 14.1 %)  9.27 c/B 203.92 c/f (+/- 5.2 %) 3.25 i/c 3.32 GHz 
boost sprit qi   :  585.51 MB/s (+/- 5.7 %) 27.91 Mfloat/s 24.86 i/B  547.00 i/f (+/- 0.0 %) 0.00 bm/B 0.00 bm/f (+/- 72.1 %)  5.42 c/B 119.23 c/f (+/- 5.3 %) 4.59 i/c 3.33 GHz 
fastfloat        : 1116.40 MB/s (+/- 5.9 %) 53.21 Mfloat/s 11.64 i/B  256.04 i/f (+/- 0.0 %) 0.00 bm/B 0.01 bm/f (+/- 2.6 %)   2.85 c/B  62.61 c/f (+/- 5.3 %) 4.09 i/c 3.33 GHz 
fastfloat fixed  : 1339.68 MB/s (+/- 2.5 %) 63.85 Mfloat/s  9.86 i/B  217.02 i/f (+/- 0.0 %) 0.00 bm/B 0.00 bm/f (+/- 3.9 %)   2.38 c/B  52.27 c/f (+/- 1.7 %) 4.15 i/c 3.34 GHz 

f:id:toge:20210131015608p:plain

Linuxだとperfの計測結果まで付与してくれて便利ですね。 Zen3でどうなるのか気になる…。(Ryzen 5800Xください)

Intel Core i5-8210Y(Macbook Air 2018) apple-clang 12.0.0

fastfloat (fake)  : 1314.89 MB/s (+/- 19.2 %) 62.67 Mfloat/s  15.96 ns/f 
netlib            :  262.55 MB/s (+/- 16.2 %) 12.51 Mfloat/s  79.91 ns/f 
doubleconversion  :  220.23 MB/s (+/- 17.5 %) 10.50 Mfloat/s  95.27 ns/f 
strtod            :   67.26 MB/s (+/- 9.2 %)   3.21 Mfloat/s 311.92 ns/f 
abseil            :  412.25 MB/s (+/- 13.9 %) 19.65 Mfloat/s  50.89 ns/f 
boost spirit qi   :  426.88 MB/s (+/- 14.3 %) 20.35 Mfloat/s  49.15 ns/f 
fastfloat         :  886.46 MB/s (+/- 14.2 %) 42.25 Mfloat/s  23.67 ns/f 
fastfloat fixed   : 1076.13 MB/s (+/- 18.9 %) 51.29 Mfloat/s  19.50 ns/f 

f:id:toge:20210131020058p:plain

MacOS Xだからperfがないのであっさり表示。
strtodの衝撃の遅さが目を惹きます。

Intel Core i5-4300U(Let's Note LX3) gcc 10.2.0

fastfloat (fake)  : 885.05 MB/s (+/- 3.7 %) 42.18 Mfloat/s  9.59 i/B  211.00 i/f (+/- 0.0 %) 0.00 bm/B 0.00 bm/f (+/- 47.6 %)  3.11 c/B  68.32 c/f (+/- 1.0 %) 3.09 i/c 2.88 GHz 
netlib            : 197.58 MB/s (+/- 1.6 %)  9.42 Mfloat/s 30.46 i/B  670.18 i/f (+/- 0.0 %) 0.19 bm/B 4.12 bm/f (+/- 5.1 %)  13.94 c/B 306.64 c/f (+/- 0.8 %) 2.19 i/c 2.89 GHz 
doubleconversion  : 146.04 MB/s (+/- 1.3 %)  6.96 Mfloat/s 50.79 i/B 1117.40 i/f (+/- 0.0 %) 0.12 bm/B 2.58 bm/f (+/- 0.5 %)  18.86 c/B 414.93 c/f (+/- 0.6 %) 2.69 i/c 2.89 GHz 
strtod            : 126.60 MB/s (+/- 1.2 %)  6.03 Mfloat/s 51.72 i/B 1137.85 i/f (+/- 0.0 %) 0.15 bm/B 3.20 bm/f (+/- 0.2 %)  21.76 c/B 478.70 c/f (+/- 0.5 %) 2.38 i/c 2.89 GHz 
abseil            : 363.70 MB/s (+/- 1.3 %) 17.33 Mfloat/s 26.93 i/B  592.50 i/f (+/- 0.0 %) 0.02 bm/B 0.50 bm/f (+/- 0.3 %)   7.58 c/B 166.69 c/f (+/- 0.5 %) 3.55 i/c 2.89 GHz 
boost spirit qi   : 543.73 MB/s (+/- 1.2 %) 25.92 Mfloat/s 21.09 i/B  464.00 i/f (+/- 0.0 %) 0.00 bm/B 0.00 bm/f (+/- 53.3 %)  5.07 c/B 111.44 c/f (+/- 0.2 %) 4.16 i/c 2.89 GHz 
fastfloat         : 729.81 MB/s (+/- 1.7 %) 34.78 Mfloat/s 12.14 i/B  267.04 i/f (+/- 0.0 %) 0.00 bm/B 0.01 bm/f (+/- 0.5 %)   3.78 c/B  83.11 c/f (+/- 0.3 %) 3.21 i/c 2.89 GHz 
fastfloat fixed   : 824.43 MB/s (+/- 1.3 %) 39.29 Mfloat/s 10.23 i/B  225.02 i/f (+/- 0.0 %) 0.00 bm/B 0.00 bm/f (+/- 1.3 %)   3.34 c/B  73.54 c/f (+/- 0.4 %) 3.06 i/c 2.89 GHz 

f:id:toge:20210131020251p:plain

fastfloatが少し落ち込んでますが、全体的な傾向は変わらず。

Qualcomm Snapdragon 720G(Redmi Note 9S) clang 11.0.0

fastfloat (fake)  : 919.94 MB/s (+/- 1.7 %) 43.85 Mfloat/s
netlib            : 199.87 MB/s (+/- 0.7 %)  9.53 Mfloat/s
doubleconversion  : 119.26 MB/s (+/- 1.0 %)  5.68 Mfloat/s
strtod            :  29.22 MB/s (+/- 0.3 %)  1.39 Mfloat/s
abseil            : 241.33 MB/s (+/- 1.6 %) 11.50 Mfloat/s
boost spirit qi   : 321.52 MB/s (+/- 1.4 %) 15.32 Mfloat/s
fastfloat         : 575.66 MB/s (+/- 1.0 %) 27.44 Mfloat/s
fastfloat fixed   : 714.75 MB/s (+/- 1.0 %) 34.07 Mfloat/s

f:id:toge:20210131020419p:plain

これもTermux上なのでperfがないっぽい。
MacOSXと同じくstrtodが遅い。どんな実装なのか気になる…。

まとめ

全体的な速度で比較するとこんな感じでしょうか。

strtod <= doubleconversion < netlib < abseil <= boost spirit qi << fastfloat < fastfloat fixed

fastfloat圧倒的ですね。
Apache2.0ライセンスだってことが気になるぐらいで、大きな弱点も見当たらないので基本的にはfastfloat使えばいいと思いました。
昔は速いと思っていたdoubleconversionがstrtodと対して変わらないのはちょっとびっくりでした。

ただし、std::from_charsが普通に使えるようになったり、内部の実装が大きく改善することは今後当然考えられるので、あくまで「私が使った環境に依存している」ことはご留意ください。
ベンチマークを試すのは比較的簡単なので、技術選定の際にはご自身の環境での評価をオススメします。

cmakeでHeader Onlyライブラリをお手軽に使う

FetchContentを使う動機

cmakeでHeader Onlyライブラリをちょっと使いたい場合ありますよね。
git submoduleでいいのですが、ちょっと管理が煩雑だなぁと思っています。

普段はなるべくConanパッケージを作る→使うのですが、お試しでマイナーなライブラリ使うときには仰々しいかなぁと思ったりします。

そこでCMake 3.11から導入されたFetchContent機能を利用してみました。 cmake.org

FetchContentはかなり柔軟に書くことができて、gitで指定ブランチもtar.gz/zipの取得・展開もできます。
私は「なるべくリリースされたものを使いたい」という考え方なので、リリースされたものを利用することにします。

今回はちょうど最近リリースされたCTML 2.0.0を触ってみたかったのでそれを例にしてみます。
※CTMLはHeader OnlyのHTML生成ライブラリです。

CMakeLists.txtの書き方

FetchContent機能を使ったCMakeLists.txtの例です。

project(ctml CXX)
cmake_minimum_required(VERSION 3.11.0)

set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} -std=c++17")

include(FetchContent)
FetchContent_Populate(
  ctml
  URL        https://github.com/tinfoilboy/CTML/archive/2.0.0.tar.gz
  URL_HASH   MD5=936e08167384fee9a4445fed343e83dc
)

include_directories(${ctml_SOURCE_DIR}/include)

add_executable(test test.cpp)

本当はFetchContent_DeclareFetchContent_MakeAvailable を組み合わせるべきなんでしょうが、簡単にするため FetchContent_Populate だけですませています。

CMakeの実行と結果の確認

CMakeLists.txtの存在するディレクトリでcmakeを実行してみます。

% cmake -B build --verbose -S .
-- The CXX compiler identification is AppleClang 12.0.0.12000032
-- Check for working CXX compiler: /Library/Developer/CommandLineTools/usr/bin/c++
-- Check for working CXX compiler: /Library/Developer/CommandLineTools/usr/bin/c++ -- works
-- Detecting CXX compiler ABI info
-- Detecting CXX compiler ABI info - done
-- Detecting CXX compile features
-- Detecting CXX compile features - done
-- Populating ctml
-- Configuring done
-- Generating done
-- Build files have been written to: /Users/toge/src/ctml/build/ctml-subbuild
Scanning dependencies of target ctml-populate
[ 11%] Creating directories for 'ctml-populate'
[ 22%] Performing download step (download, verify and extract) for 'ctml-populate'
-- Downloading...
   dst='/Users/toge/src/ctml/build/ctml-subbuild/ctml-populate-prefix/src/2.0.0.tar.gz'
   timeout='none'
-- Using src='https://github.com/tinfoilboy/CTML/archive/2.0.0.tar.gz'
* Closing connection 0
-- verifying file...
       file='/Users/toge/src/ctml/build/ctml-subbuild/ctml-populate-prefix/src/2.0.0.tar.gz'
* Closing connection 1
-- Downloading... done
-- extracting...
     src='/Users/toge/src/ctml/build/ctml-subbuild/ctml-populate-prefix/src/2.0.0.tar.gz'
     dst='/Users/toge/src/ctml/build/ctml-src'
-- extracting... [tar xfz]
-- extracting... [analysis]
-- extracting... [rename]
-- extracting... [clean up]
-- extracting... done
[ 33%] No patch step for 'ctml-populate'
[ 44%] No update step for 'ctml-populate'
[ 55%] No configure step for 'ctml-populate'
[ 66%] No build step for 'ctml-populate'
[ 77%] No install step for 'ctml-populate'
[ 88%] No test step for 'ctml-populate'
[100%] Completed 'ctml-populate'
[100%] Built target ctml-populate
-- Configuring done
-- Generating done
-- Build files have been written to: /Users/toge/src/ctml/build

こうすると、buildディレクトリの下が次のようになります。

% find build -type d 
build
build/ctml-src
build/ctml-src/include
build/ctml-src/tests
build/ctml-src/.github
build/CMakeFiles
build/CMakeFiles/3.15.3
build/CMakeFiles/CMakeTmp
build/CMakeFiles/test01.dir
build/ctml-subbuild
build/ctml-subbuild/CMakeFiles
build/ctml-subbuild/CMakeFiles/ctml-populate.dir
build/ctml-subbuild/CMakeFiles/3.15.3
build/ctml-subbuild/ctml-populate-prefix
build/ctml-subbuild/ctml-populate-prefix/tmp
build/ctml-subbuild/ctml-populate-prefix/src
build/ctml-subbuild/ctml-populate-prefix/src/ctml-populate-stamp
build/ctml-build

ちゃんとctml-srcの下にCTML-2.0.0.tar.gzを展開した結果が配置されてます。

ソースコードでの参照方法

build/ctml-src というパスは <name>_SOURCE_DIR というCMakeの変数で参照可能になっています。
nameはFetchContent_Populateの最初の引数で指定しているので、今回の場合はctmlですね。
このため、今回は次のようにinclude_directoryを指定しています。

include_directories(${ctml_SOURCE_DIR}/include)

これでソースコードの中でCTMLのヘッダをインクルードできます。

#include <iostream>

#include "ctml.hpp"

int main(int argc, char* argv[]) {
  ...

まとめ

CMakeのFetchContentを利用してHeader Onlyライブラリを利用する方法を説明しました。

FetchContentではconfigure→build→testの処理も定義できるみたいなのですが、今回はHeader Onlyライブラリなので何も指定していませんでした。
機会があればビルド必須なライブラリをFetchContentで利用してみようと思います。

C++のロギングライブラリのメモ

昨年末の話になりますが、ぼーっとredditC++カテゴリを見ていたらlwlogというライブラリの話題が出てました。 「spdlogより速い」っていうのが売りのようです。確かに速そう。 www.reddit.com

ちょうど良いのでロギングライブラリの個人的なメモをしておこうと思います。

boost log

www.boost.org

C++の定番ライブラリboostのloggingライブラリです。 機能豊富ですが、残念ながら大分遅いです。 C++ 的に遅いのはかなり致命的だなぁと思うのであまり積極的には使いません。 boost以外使えない場合とかあるのでそういう時に重宝します。

spdlog

github.com

多分boost logを含めた昔からあるloggingライブラリを駆逐する高速処理のロギングライブラリでした。 今となってはもっと速いライブラリが出てきてしまったので、処理速度では残念な感じです。 ただ対応している出力先が豊富なので、巨大なシステムで、ログをどこかに飛ばすみたいな用途だとspdlogは便利かもしれません。

設定ファイルでsinkの構成を定義できる guangie88/spdlog_setup というライブラリがあるのが重宝します。 github.com

quill

github.com

私としては最近一番使っているロギングライブラリになります。

  • 最低限の機能がある
  • 簡単に使える
  • 高速に動作する
  • ログファイルがテキストで扱いやすい
  • Conanパッケージがある といったところが気に入ってます。

ログローテートなどはあまり凝ったことはできませんので、そういうのを求めるならspdlogなのかなぁと思います。

Nanolog

github.com

quillよりもちょっとだけ速いロギングライブラリ。 いろいろ野心的で、好奇心をくすぐられる部分も多いのですが、以下の点が個人的には大きなデメリットです。

  • Linuxのみに対応
  • ログファイルが独自バイナリで閲覧時に専用コマンドでテキスト化

quillよりも圧倒的に速いとかなら考えるのですが、ベンチマーク見ると10%も変わらないです。 今のところ積極的に使う理由が無いなぁと。

lwlog

github.com

期待の新星。 sinkの設定をcompile timeで構成できるのが速そう。

今のところVisual C++でのビルドが前提になってしまっているけれど、CMake対応してくれるみたいです。 手軽に使えるようになったら是非使ってみたい。

Conanで依存関係の衝突にあって対処した話

直近困ったのでメモ。

TL;DR

  • Conanでパッケージ衝突がおきたら、パッケージ依存グラフ機能を使おう
  • 自分でレシピを書くことがあったら、依存パッケージのバージョンはできるだけ範囲指定してほしい(願望)

OpenCVとTesseract OCRを使ってみたかった

相変わらずC++で何かをする時にはパッケージマネージャとしてConanを使っています。 OpenCVとTesseract OCRを同時に利用したい場合が出てきたんで、こんなconanfile.txtを書くことになりました。

[requires]
opencv/4.5.1
tesseract/4.1.1

[generators]
cmake

で、このconanfile.txtがあるディレクトリでconan install .すると、こんなエラーが出てしまいます。

% conan install .
Configuration:
[settings]
arch=x86_64
arch_build=x86_64
build_type=Release
compiler=gcc
compiler.libcxx=libstdc++11
compiler.version=10.2
os=Linux
os_build=Linux
[options]
[build_requires]
[env]

WARN: libtiff/4.1.0: requirement libwebp/1.1.0 overridden by leptonica/1.79.0 to libwebp/1.0.3 
ERROR: libtiff/4.1.0: Incompatible requirements obtained in different evaluations of 'requirements'
    Previous requirements: [zlib/1.2.11, xz_utils/5.2.5, libjpeg/9d, jbig/20160605, zstd/1.4.5, libwebp/1.1.0]
    New requirements: [zlib/1.2.11, xz_utils/5.2.5, libjpeg/9d, jbig/20160605, zstd/1.4.5, libwebp/1.0.3]

うーん、よく分からないですね。 OpenCVとtesseractなのに、libtiffがlibwebpのこと文句言ってるし…。

パッケージ依存グラフで原因を調べてみる

こういうのは大抵パッケージの依存パッケージの衝突が原因です。 Conanのパッケージ依存グラフ機能(experimental)を使うと図で表示されて便利だったりします。 docs.conan.io ※Graphizのインストールが必要になります。

OpenCVの依存グラフ

最初にOpenCVの依存グラフ見てみましょう。

% conan info opencv/4.5.1@ --graph=opencv.dot   
% dot -Tpng opencv.dot > opencv.png   

f:id:toge:20210116024447p:plain
OpenCVの依存グラフ

OpenCV -> libtiff -> libwebpの依存が見えますね。

Tesseract OCRの依存グラフ

次はTesseract OCRの依存グラフです。

% conan info tesseract/4.1.1@ --graph=tesseract.dot    
% dot -Tpng tesseract.dot > tesseract.png   

f:id:toge:20210116024733p:plain
Tesseract OCRの依存グラフ

webpについて2つの依存経路がありますね。

  • Tesseract OCR -> leptonica -> libtiff -> libwebp
  • Tessearct OCR -> leptonica -> libwebp

そしてwebpのバージョンが1.0.3に変化しています。

なぜこんなことが起きるかというと、leptonicaのConanレシピがwebpのバージョンを1.0.3に決め打ちしているためです。

https://conan.io/center/leptonica?version=1.79.0&tab=recipe

        if self.options.with_webp:
            self.requires("libwebp/1.0.3")

そうするとleptonicaが依存するlibtiffも空気を読んでwebpのバージョンを1.0.3に変更してしまっているんです。 一方でOpenCV側で依存しているlibtiffは特に制約がないのでレシピの通り1.1.0を使います。

https://conan.io/center/libtiff?version=4.1.0&tab=recipe

        if self.options.get_safe("webp"):
            self.requires("libwebp/1.1.0")

これで見事に「libtiffが依存するlibwebpのバージョンが衝突する」という事態が起きてしまいました。

回避策

回避策は何個かあると思います。 私が考えついたのは以下の2つ。

  • libtiffレシピのoptionでwebpを無効にする
  • leptonicaレシピのoptionでwebpを無効にする

他のプロジェクトでの汎用性を考えると、libtiffを使う場合に毎回webpを無効にするオプションをつけるのは面倒くさいので、(そうしないとlbtiffのバイナリが2バージョンできてしまう)今回はleptonicaレシピのoptionでwebpを無効にすることにします。 どうせ画像の読み書きはOpenCVでやるのでleptonicaの機能が減っても困りませんし。

% conan info tesseract/4.1.1@ -o leptonica:with_webp=False --graph=tesseract_wo_webp.dot
% dot -Tpng tesseract_wo_webp.dot > tesseract_wo_webp.png   

f:id:toge:20210116030821p:plain
webpを無効化したTesseract OCRの依存グラフ

ちゃんとlibwebpのバージョンが1.1.0になりました。 これならOpenCVと衝突しなくなります。

conanfile.txtに結果を反映

次のようなconanfile.txtにすることで、衝突なくinstallができるようになりました。

[requires]
opencv/4.5.1
tesseract/4.1.1

[generators]
cmake

[options]
leptonica:with_webp=False

補足: 本来はどうあるべきか?

汚い逃げ方をしてしまいましたが、本来はレシピがお行儀良く依存関係を書いてくれていればいいのになぁと思います。 Conanでは依存パッケージのバージョン番号を範囲で指定できるのでleptonicaもlibtiffも範囲指定してくれていれば良かったんですよね。

        if self.options.with_webp:
            self.requires("libwebp/[>=1.0.3]")

もちろんバージョンによってAPIや内部動作が変わるので、細かいバージョンの組み合わせをいちいちチェックしてられないのだろうとは思います。

そう考えると今回のようなテクニックを使う場面はまだまだありそうですね。

xcodeをバージョンアップしてもapple clangがアップデートされるとは限らない

xcodeを12.3にすることができて一安心していたのですが、 apple clangが11.0のままだったことに気がつきました。 どうやらxcodeがアップデートしても、apple clangがアップデートされるわけではないみたいですね。

こちらの内容を元に以下のコマンドでアップデートしました。 python5.com

sudo rm -rf /Library/Developer/CommandLineTools
xcode-select --install

めでたくapple clangが12.0になったら、今度はConanのboostパッケージがビルドできない・・・。 どうやらレシピのバグらしくて、レシピのアップデート待ちです。 github.com

xcodeはバージョンアップするといつも何か起きてしまうなぁ。

2021-01-24 追記

Conanのboostレシピが更新されて boost 1.74.0 だったらapple clangでもビルドできるようになりました。 まだboost 1.75.0は駄目みたいです。

conanfile.txtで条件分岐したい場合はconanfile.pyを使う

TL;DR

conanパッケージに関して環境依存で細かい条件分岐をしたい場合はconanfile.txtではなくconanfile.pyを使いましょう。 Pythonコードでやりたい放題です。

導入というかtermuxの話

termux面白いですね。 普段持ち歩くAndroidでほぼ完璧なLinux環境が作れてしまうワクワク感がたまりません。

最近termuxで開発環境を作るのが楽しいです。 そこで困るのがtermuxの特殊なコンパイル環境です。

最近になってclang version 11が入ってほぼ最新のC++環境が手に入るのですが、微妙にx86_64とは異なる勝手があって困ることがあります。 今日困ったけれどなんとか解決できたものがあったのでメモっておきます。

足りないヘッダファイルがあってboostがコンパイルできない

私はC++のパッケージマネージャーの一つであるConanを利用しています。 conan.io Termuxに限らず、色々な環境でビルドする時にライブラリのバージョンを揃えたり、マイナーなライブラリを利用する際にとても便利です。

TermuxでもConanを使ってboostライブラリの最新版を使ってみようと思ったのですが、見事にハマりました。 strfmon() を定義している monetary.h というヘッダがないのでboost localeとboost logがビルドできないのです。

libs/locale/src/posix/numeric.cpp:26:10: fatal error: 'monetary.h' file not found
#include <monetary.h>
         ^~~~~~~~~~~~
1 error generated.

conanでパッケージオプションを指定する方法

boostCMakelists.txt側で認識してくれていないのが謎ですが monetary.h が無い環境なんてかなりマイナーなのでしょう。 私としてはとにかく今利用できないのが困ります。

幸いboost locale, boost logを使っていないプロジェクトが多いので、そういうプロジェクトだけはTermuxでビルドできるようにしてみたいと思いました。

boost locale, boost logだけをビルドの対象から外すのはconanコマンドが読み込むconanfile.txtにパッケージオプションを指定するだけです。

[requires]
boost/1.75.0

[generators]
cmake

[options]
boost.without_locale = True
boost.without_log = True

monetary.hがない時だけパッケージオプションを指定したい

上の方法でとりあえずコンパイルできるのですが monetary.hがある環境ではboost locale, boost logもビルドしてほしいのです。 そうしないと~/.conan/data/boost//1.75.0/_/_/packageの下に「全ビルド版」と「locale, log除外版」の複数のバイナリが生成されてしまってboostの性質上やたらディスク使用量が大きいのが嫌だなぁと。(貧乏性です)

こういう環境などによる条件分岐にconanfile.txtは対応していません。 幸いconangithubに同じような質問をしている人がいました。 github.com

なるほどconanfile.txtと同じようにインストールするパッケージ方法としてconanfile.pyがあるんですね。 知らなんだ。 docs.conan.io

conanfile.pyの中では普通にPythonが書けてしまうのでmonetary.hの存在チェックも可能になります。 ひとまず以下のようなconanfile.pyを書いてやりたいことを実現できるようになりました。

from conans import ConanFile
import subprocess

class ProjectConan(ConanFile):
   settings = "os", "arch", "compiler"
   generators = "cmake"

   def requirements(self):
       self.requires("boost/1.75.0")

   def config_options(self):
       compiler = str(self.settings.compiler)
       # check monetary header existance
       if self.settings.os == "Linux" and (compiler == "gcc" or compiler == "clang"):
           monetary_check = subprocess.run([compiler, "-E", "-x", "c", "-"], input="#include<monetary.h>", text=True, stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL)
           if monetary_check.returncode != 0:
               self.options["boost"].without_locale = True
               self.options["boost"].without_log = True

まとめ

  • ConanでのC++パッケージ管理便利です。
  • conanfile.txtでパッケージ定義が基本ですが、条件分岐したければconanfile.pyPythonコードを書けます。