std::string文字列がfloatになり得るかどうかをチェックしつつfloatへの変換を行う場合、通常はstd::stofを用いて
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 |
// Copyright SCIEMENT, Inc. // by Hirofumi Seo, M.D., CEO & President #include <string> #include <iostream> float stofTryCacth(const std::string& a, bool* is_float) { float result = 0.0f; try { result = std::stof(a); } catch (const std::invalid_argument&) { std::cout << "Error: The string '" << a << "' is not float." << std::endl; *is_float = false; return result; } catch (const std::out_of_range&) { std::cout << "Error: The string '" << a << "' is float but out of range." << std::endl; *is_float = false; return result; } *is_float = true; std::cout << "'" << a << "' -> " << result << std::endl; return result; } int main() { float result; bool is_float; result = stofTryCacth("3.14", &is_float); // '3.14' -> 3.14 result = stofTryCacth(" 3.14", &is_float); // ' 3.14' -> 3.14 result = stofTryCacth("3.14 ", &is_float); // '3.14 ' -> 3.14 result = stofTryCacth(" 3.14 ", &is_float); // ' 3.14 ' -> 3.14 result = stofTryCacth("3.14abc", &is_float); // '3.14abc' -> 3.14 result = stofTryCacth("abc3.14", &is_float); // Error: The string 'abc3.14' is not float. result = stofTryCacth("abc", &is_float); // Error: The string 'abc' is not float. result = stofTryCacth("3.14e2", &is_float); // '3.14e2' -> 314 result = stofTryCacth("3.14e-2", &is_float); // '3.14e-2' -> 0.0314 result = stofTryCacth("314.e-2", &is_float); // '314.e-2' -> 3.14 result = stofTryCacth(".314e-2", &is_float); // '.314e-2' -> 0.00314 result = stofTryCacth("3.14e100", &is_float); // Error: The string '3.14e100' is float but out of range. result = stofTryCacth("-3.14e100", &is_float); // Error: The string '-3.14e100' is float but out of range. return 0; } |
のように行うかと思います。ただ、このままですと35行目にあるように小数の後に余計な文字列が連続しているような場合でも小数として生成されてしまいますので、
厳密にはstd::stofの第二引数 size_t* _Idxを用いてゴニョゴニョする必要があります。
約10万個のfloat文字列をfloat型に変換するのに560 msec程度で終わりました。約0.5秒ちょっとです。
ちなみにtry / catchを使わずに単にstd::stofを用いてしまうと
1 2 3 4 5 6 7 8 9 10 11 |
// Copyright SCIEMENT, Inc. // by Hirofumi Seo, M.D., CEO & President #include <string> #include <iostream> int main() { const float result = std::stof("abc"); // ここでフリーズしてしまう。 std::cout << "abc" << result << std::endl; return 0; } |
上記のように、floatに変換出来ない値を与えるとフリーズしてしまいます。
で、残念なことにUnreal Engineではtry / catchの使用は推奨されておらず、デフォルトでは無効になっています。
一応、回避方法はあるのですが、
参考記事:
How can I enable unwind semantics for C++-style exceptions? – UE4 AnswerHub
バージョンごとに指定方法が異なるなど、やはり無理やりな方法である感は否めません。
となるとtry / catchを使わずにstd::string -> floatへの変換を安全に行いたいわけです。
まず思いつく方法は、正規表現を駆使する方法です。ここは素直に偉大なる先人たちの知恵を使いましょう。
参考記事:
Example: Matching Floating Point Numbers with a Regular Expression
1 2 3 4 5 6 7 8 9 10 11 |
// Copyright SCIEMENT, Inc. // by Hirofumi Seo, M.D., CEO & President #include <string> #include <regex> bool CheckIsFloat(const std::string& x) { std::regex e1("[-+]?[0-9]*[.]?[0-9]+([eE][-+]?[0-9]+)?"); std::regex e2("[-+]?[0-9]+[.]"); return std::regex_match(x, e1) || std::regex_match(x, e2); } |
みたいな関数を作ってやって確認すればOKです。ただ、少なくとも私の環境では、この方法は速度が非常に遅くなってしまいました。
上記と同じ約10万個のfloat文字列をfloat型に変換するのに4268 msecも要してしまいました。約4秒です。
しかも、正確にはfloatの範囲内に小数の値が収まっているかどうかなどのチェックもこの後追加で行う必要があります。
//——2018/5/15加筆——
上記正規表現を用いた方法に関して、以下のようなコメントを頂きました。
記事の本質とは少しずれますが、std::regexがこの使い方で極めて遅いのはstd::regexを関数呼び出しの度に生成している影響が支配的だと思うので、 regex を static で定義するだけでそこは4桁くらい速くなると思う。いま wandbox が 405 吐くようなので実験は省略するけど・w・ https://t.co/ugjOTdcF0y
— デス味(デス・ウェイ)うさぎ // Usagi Ito (@USAGI_WRP) 2018年5月15日
…た、確かに…!値が変わらない定数はstatic constで良いという大鉄則を完全に見落としておりました…。恥ずかしい…。
というわけで以下のように改良してみると…
1 2 3 4 5 6 7 8 9 10 11 |
// Copyright SCIEMENT, Inc. // by Hirofumi Seo, M.D., CEO & President #include <string> #include <regex> bool CheckIsFloat(const std::string& x) { static const std::regex e1("[-+]?[0-9]*[.]?[0-9]+([eE][-+]?[0-9]+)?"); static const std::regex e2("[-+]?[0-9]+[.]"); return std::regex_match(x, e1) || std::regex_match(x, e2); } |
上記と同じ約10万個のfloat文字列をfloat型に変換するのに2779 msecでした。約2.8秒になりました。
regexの生成って重いのですね…。しかしそれでも後述のstrtofのほうがやはりずっと速い結果にはなりました。
//————————
【C++教えて】文字列がfloat型かチェックするのに
"[-+]?[0-9]*[.]?[0-9]+([eE][-+]?[0-9]+)?"と"[-+]?[0-9]+[.]"でのregex_matchで可能なのですが、std::stofのtry, catchのほうが圧倒的に速いです。で、try / catchを使わずにstofの結果が正しいかどうかをチェックする方法ってあるのでしょうか…?— Hirofumi Seo (@HirofumiSeo) 2018年5月14日
すると、
strtod 系の関数はいかがでしょうか?
— nsɥızɐʞı (@activecontour) 2018年5月14日
というお返事を頂きました!
なるほど確かにstd::stofの仕様にも
The function uses strtod (or wcstod) to perform the conversion (see strtod for more details on the process).
と書かれておりました。
参考記事:
stof – C++ Reference – cplusplus.com
今回はfloatにしたいので、strtodではなくstrtofを使えば良さそうです。
と、いうわけで、strtofの仕様を読みながら書けば終わりです。
strtof – C++ Reference – cplusplus.com
・第一引数const char* strに小数以外の文字列が含まれる場合、且つ、第二引数char** endptrにNULL以外の値を与えたとき、”A pointer to the rest of the string after the last valid character is stored in the object pointed by endptr.”とのことで、endptrにその文字列が入る。
・float値の範囲に収まらない場合は、-HUGE_VALF or HUGE_VALFを返す。
あたりを意識すれば書けそうです。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 |
// Copyright SCIEMENT, Inc. // by Hirofumi Seo, M.D., CEO & President #include <string> #include <cstdlib> #include <iostream> float StdStringToFloat(const std::string& x, bool* is_float) { char* not_float_x; const float result = std::strtof(x.c_str(), ¬_float_x); const size_t length = std::strlen(not_float_x); if (length == 0) { if (result == HUGE_VALF || result == -HUGE_VALF) { // out of float range. std::cout << "Error: The string '" << x << "' is float but out of range." << std::endl; *is_float = false; return result; } std::cout << "'" << x << "' -> " << result << std::endl; *is_float = true; return result; } bool are_all_space = true; for (size_t i = 0; i < length; ++i) { if (not_float_x[i] != ' ') { are_all_space = false; break; } } if (are_all_space) { std::cout << "'" << x << "' -> " << result << std::endl; *is_float = true; return result; } std::cout << "Error: The string '" << x << "' is not float." << std::endl; *is_float = false; return result; } int main() { float result; bool is_float; result = StdStringToFloat("3.14", &is_float); // '3.14' -> 3.14 result = StdStringToFloat(" 3.14", &is_float); // ' 3.14' -> 3.14 result = StdStringToFloat("3.14 ", &is_float); // '3.14 ' -> 3.14 result = StdStringToFloat(" 3.14 ", &is_float); // ' 3.14 ' -> 3.14 result = StdStringToFloat("3.14abc", &is_float); // Error: The string '3.14abc' is not float. result = StdStringToFloat("abc3.14", &is_float); // Error: The string 'abc3.14' is not float. result = StdStringToFloat("abc", &is_float); // Error: The string 'abc' is not float. result = StdStringToFloat("3.14e2", &is_float); // '3.14e2' -> 314 result = StdStringToFloat("3.14e-2", &is_float); // '3.14e-2' -> 0.0314 result = StdStringToFloat("314.e-2", &is_float); // '314.e-2' -> 3.14 result = StdStringToFloat(".314e-2", &is_float); // '.314e-2' -> 0.00314 result = StdStringToFloat("3.14e100", &is_float); // Error: The string '3.14e100' is float but out of range. result = StdStringToFloat("-3.14e100", &is_float); // Error: The string '-3.14e100' is float but out of range. return 0; } |
24~30行目がポイントで、小数のあとに空白のみが続いている場合は49, 50行目にあるようにきちんと小数とみなされ、小数のあとに小数でない文字列が続くときは、最初のstd::stof with try / catchでは小数とされてしまったものが、51行目にあるように小数でないとみなされるようになりました。
ちなみに上記と同じ約10万個のfloat文字列をfloat型に変換するのに505 msec程度で終わりました。約0.5秒です。
std::stofよりも若干速くなったのは、throwが無くなったから?なのでしょうか??
これでUnreal Engineでも高速且つ安全にstd::string -> floatへの変換処理が出来ますね!
ちなみに自分が調べた限りでは、Unreal EngineでFString -> floatに「安全に」変換する関数は無かったようでした。
近いものでbool FString::IsNumeric()という関数はあるのですが、これは整数値のみに有効です。