hsimyu's diary

ゲームなどをします。

9/17 レビュースターライトはこれから観ます

9/17

昼に起きてご飯食べて食器洗いして本読んで掃除してご飯食べて

寝ようかな

ごはん

朝(昼): 豚汁とごはん

夜: 豚汁と冷凍からあげとごはん

最悪なる災厄人間へ捧ぐ

www.youtube.com

よさそ〜

C++17 におけるコンパイル時文字列パースの実装

背景: "f:5" という文字列が入力されたら float 型の 5.0f を返す、"i:1" という文字列が入力されたら int 型の 1 を返す constexpr 関数を書きたい

https://blog.therocode.net/2018/09/compile-time-string-parsingblog.therocode.net

  • 詳細:
      template <size_t Length>
      constexpr auto parseNumber(const char (&text)[Length])
      {
          if(text[0] == 'i')
          {
              // int 型を返す
              return strToInt(text + 2);
          }
          else if(text[0] == 'f')
          {
              // float 型を返す
              return strToFloat(text + 2);
          }
          else
          {
              // エラー???
              return 0;
          }
      }

ナイーヴな実装は上記のようになる。が、

  • 問題点: constexpr を引数に適用することはできない ⇒ 入力に対して if constexpr できない
  • 問題点2: if constexpr が使えないので auto が型を一つに絞れない、コンパイルエラー
  • 問題点3: 同様の理由で、入力に対して static_assert も出来ない

テンプレート引数に文字列を取るのはどうだろう?

template <const char* text>
void parseText()
{
    // ...
}

ダメです。

parseText<"test">();
error: '"test"' is not a valid template argument for type 'const char*' because string literals can never be used in this context

Damn, so string literals are just simply forbidden within template brackets.

また、 template<ConstantString text> のような形でプリミティブ型以外の型(の値を)テンプレートパラメータとして渡すことも想定されていない。

Solution!

constexpr lambda を使おう!

C++ 17 から どんなラムダでも (constexpr 関数の条件を満たせば) constexpr にすることができる 上に non-constexpr なものを使わない限り、デフォルトで constexpr である という変化がある。

      template<typename StringHolder>
      constexpr void parseText(StringHolder holder>
      {
        // 受け取ったラムダの返り値を constexpr で受ける
        constexpr std::string_view text = holder();
        // constexpr 変数なので static_assert 可能!
        static_assert(text[0] == 'i' || text[0] == 'f');
      }
      
      void f()
      {
        // 静的な文字列を返すだけのラムダを渡す => constexpr でラップして渡すことができる!
        parseText( [](){ return "i:5"; } );
      }

if constexpr と組み合わせることも勿論可能です。つよいぜ

ラムダ化するところをマクロ化すれば、普通の関数みたいに書ける。

      #define PARSE_TEXT(text) \
      parseText( [](){ return text; })
      
      void f()
      {
        PARSE_TEXT("i:5");
      }

最終実装

      #include <type_traits>
      #include <string_view>
      
      template <typename StringHolder>
      constexpr auto parseNumber(StringHolder holder)
      {
          constexpr std::string_view text = holder();
      
          // 数値1桁だけサポートする
          static_assert(text.size() == 3, "invalid length of input");
      
          constexpr char typeChar = text[0];
          constexpr char numberChar = text[2];
      
          // サポートする型は i か f のみ
          static_assert(typeChar == 'i' || typeChar == 'f', "must start with 'i' or 'f'");
            // 中央はコロン分けされていないといけない
          static_assert(text[1] == ':', "lacks proper ':' delimeter");
      
          // 数値は 0-9 のみ
          static_assert(numberChar >= '0' && numberChar <= '9', "number part is not a valid number");
      
            // ASCII to Number 変換
          int resultingNumber = numberChar - '0';
          if constexpr(typeChar == 'i')
              return resultingNumber;
          else
                    // float 型指定なら static_cast する
              return static_cast<float>(resultingNumber);
      }
      
      #define PARSE_NUMBER(text) \
      parseNumber([](){return text;})
      
      void usage()
      {
          auto intResult = PARSE_NUMBER("i:2");
          auto floatResult = PARSE_NUMBER("f:5");
      
            // テスト
          static_assert(std::is_same_v<decltype(intResult), int>);
          static_assert(std::is_same_v<decltype(floatResult), float>);
      }

コメント欄: こういうライブラリもあるぜ: https://github.com/SephDB/constexpr-format

C++: std::basic_string_view

C++17 から使える文字列 STL ライブラリ。

basic_string_view - cpprefjp C++日本語リファレンス

詳細:

std::basic_string_viewは、文字列の所有権を保持せず、文字列のコピーを持つのではなく参照をして、参照先の文字列を加工して扱うクラスである。

文字配列型である文字列リテラルに対して、[**std::basic_string](https://cpprefjp.github.io/reference/string/basic_string.html)クラスが持つような便利なメンバ関数群を使用できる。文字列リテラルは静的記憶域に保存される**ため、文字列リテラルをこのクラスのオブジェクトに参照させて、そのオブジェクトを持ち回ったとしても参照先の文字列リテラルの寿命が尽きるような問題は発生しない。

      string_view sv = "Hello World"; // この式の評価がおわったあとも、文字列リテラル "Hello World" の寿命は尽きない
      string_view hello = sv.substr(0, 5); // 先頭5文字を抽出する

このクラスの実装としては、文字配列の参照する先頭文字へのポインタと、文字数の2つをメンバ変数として持つ。これらの変数を変動させることによって、部分文字列の抽出や、限定された範囲内での検索といったことを実現する。

  • std::string_viewstd::basic_string_view<char>エイリアス
  • メモリが静的領域に確保されるのが最大の特徴。
      #include <iostream>
      #include <string_view>
      
      int main()
      {
        // 文字列リテラルから部分文字列を取得する。
        // その際、メモリアロケートは発生しない
        std::cout << std::string_view("Hello World").substr(0, 5) << std::endl;
      
        // 文字列リテラル内から特定の文字列を検索する。
        // この例でも、メモリアロケートや文字列オブジェクトのコピーなどは発生しない
        std::string_view sv = "Hello World";
        std::size_t pos = sv.find("rl");
        if (pos != std::string_view::npos) {
          std::cout << "found" << std::endl;
        }
      }

C++: ユーザー定義リテラル

C++11 から!

ユーザー定義リテラル - cpprefjp C++日本語リファレンス

詳細:

ユーザー定義リテラルは、operator"" サフィックス名の演算子オーバーロードする。""とサフィックス名の間にスペースが必要なので注意(C++14 では空白は必須では無くなった。リテラル演算子のスペースを省略可能とする参照)。

std::string operator"" s(const char* str, std::size_t length) {
    return std::string(str, length);
}

auto str = "hallo"s; // std::string 型の文字列が返ってくる

以下の4種類が利用可能。(受け取りシグネチャで変える?) (戻り値型で変える)

整数リテラル

  1. unsigned long long型のパラメータをひとつだけ持つリテラル演算子
  2. const char* 型のパラメータをひとつだけ持つリテラル演算子
  3. char 型のテンプレートパラメータパックを一つだけ持ち、パラメータの無いリテラル演算子テンプレート

コード例:

      namespace unit_literals {
        // intの大きさを持ち、km (kiro-meter, キロメートル)単位を表すリテラル演算子
        // 上記 1 のバージョン
        int operator"" _kmi(unsigned long long x)
        {
          return x * 1000;
        }
      
        // 上記 2 のバージョン
        int operator"" _kmj(const char* s)
        {
          return std::strtoull(s, nullptr, 10) * 1000;
        }
      
        // 上記 3 のバージョン
        template<char... S>
        int operator"" _kmk()
        {
          using CSTR = const char[];
          return operator"" _kmj(CSTR{ S..., '\0' });
        }
      }
      
      using namespace unit_literals;
      
      // 123km (123,000m)
      int distance1 = 123_kmi;
      // 456km (456,000m)
      int distance2 = 456_kmj;
      // 789km (789,000m)
      int distance3 = 789_kmk;

今日のオクトパストラベラー

久しぶりにプレイ。テリオン4章をクリア。ま、まるくなっとるー

オルベリク4章の街へ辿り着いて終了。

Brown Dust

やり始めました。面白そうな雰囲気があるが、コンテンツ量が多そうで逆に苦しい。

その他

この Dead Cells レビュー、いいっすね。

jp.ign.com

Notion からコードコピーするとインデントがコードブロックに残るの微妙だなあ。