私は普段からC++を愛用していますが,何気に文字列と数値変換に関してはよくググります.この記事では,個人的な備忘録の意味合いが大きいですが,「文字列と数値の変換」と「変換速度」についてまとめておきます.
文字列と数値の変換する場合,一見簡単なようでさまざまな問題があります.例えば,「数値でない文字列を数値に変換しようとする場合」や「文字列で表された文字列が数値型ではオーバーフローする場合」などです.
単純に標準ライブラリで用意された関数を使用すると,意図しない変換が行われる可能性があります.今回はこのような問題を潜在的に取り除いた関数を作成しました.
この記事はstd::string
を使用するため,C++11以降での動作を想定しています.それ以前に関しては,サポートしていません.
とりあえず結論は
文字列と数値変換に関してはstd::atoi
が高速ですが,危険なので使うのはやめましょう.
安全な文字列数値変換には,下記のコードを使用しよう!
数値型→文字列
int val = 1234;
std::string str = std::to_string(val);
文字列→数値型
int val = 0;
bool foo = ToValue(val, str_val); //文字列から数値変換が成功した場合はtrue,それ以外はfalse
使用している関数(ToValue()
)はこちら↓
bool ToValue(int &val, const std::string &str) {
try {
val = std::stoi(str);
return true;
} catch (...) {
return false;
}
}
bool ToValue(unsigned &val, const std::string &str) {
try {
val = std::stou(str);
return true;
} catch (...) {
return false;
}
}
bool ToValue(long &val, const std::string &str) {
try {
val = std::stol(str);
return true;
} catch (...) {
return false;
}
}
bool ToValue(unsigned long &val, const std::string &str) {
try {
val = std::stoul(str);
return true;
} catch (...) {
return false;
}
}
bool ToValue(long long &val, const std::string &str) {
try {
val = std::stol(str);
return true;
} catch (...) {
return false;
}
}
bool ToValue(unsigned long long &val, const std::string &str) {
try {
val = std::stoul(str);
return true;
} catch (...) {
return false;
}
}
bool ToValue(float &val, const std::string &str) {
try {
val = std::stof(str);
return true;
} catch (...) {
return false;
}
}
bool ToValue(double &val, const std::string &str) {
try {
val = std::stod(str);
return true;
} catch (...) {
return false;
}
}
なぜこの関数を使用するか詳細に関してはここから下に記述しています.
>> 良いコード/悪いコードで学ぶ設計入門―保守しやすい 成長し続けるコードの書き方
数値型から文字列型への変換
数値型から文字列変換では,変換の成否やオーバーフローの問題がないため,データ型に関わらず下記の関数で変換することが可能です.
int val = 1234;
std::string str = std::to_string(val);
文字列型から数値型
std::atoiの危険性
数値に変換できない文字列が与えられた場合,0
が返り値となります.また,文字列が整数型の範囲を超えた場合,オーバーフローした値が返り値になります.プログラマが想定していない返り値となるため,正常な変換が行われたか判断ができません.そのため,std::atoi
系統の変換は推奨しません.
文字列およびオーバーフローする場合のstd::atoi
挙動を確認します.
数値ではない文字列を変換する場合
文字列"foo"
を数値に変換する場合は,返り値として0
が与えられます.
std::atoi("foo") // 返り値は,0
文字列数値が変換先のデータ型を超える(オーバーフローする)場合
32bit整数型int
の範囲は「-2147483648~2147483647」となります.
std::atoi("2147483647") // 返り値は,2147483647
std::atoi("2147483648") // 返り値は,-2147483648 (オーバーフローする)
std::atoi("2147483649") // 返り値は,-2147483647 (オーバーフローする)
std::atoiの代わりにstd::stoiを使用すべき
std::atoi
は上述したような危険性があるため,別の方法としてstd::stoi
を使用します.
std::atoi
はchar
型をint
型に変換するものでしたが,std::stoi
はstd::string
型に変換するものです.char
型はC言語の名残で残っていますが,C++11以降ではstd::string
型の方が圧倒的に使い勝手が良いです.さらにstd::stoi
は例外処理に対応しているため,意図しない挙動を防ぐことが出来ます.
std::stoi("2147483647") // 返り値は,2147483647
std::stoi("2147483648") // 例外が発生,std::out_of_range
std::stoi("foo") // 例外が発生,std::invalid_argument
例外発生時はクラッシュしますが,クラッシュさせたくない場合は例外処理により処理できます.try...catch
で処理を決定します.最も簡単な例外処理は,変換の成否をbool
値の返り値とする場合です.実装に関しては後述します.
文字列をさまざまな整数型,浮動小数点数型とに変換する
std::stoi
は32bit整数型のint
型のための関数です.
64bit整数型long
や符号なし整数型unsigend
型,unsigend long
型などさまざまな型への変換方法を以下にまとめておきます.
string → int | std::stoi |
string → long | std::stol |
string → long long | std::stoll |
string → unsigned int | 定義されていない(後述) |
string → unsigned long | std::stoul |
string → unsigned long long | std::stoull |
string → float | std::stof |
string → double | std::stod |
unsinged int用std::stouはない?
unsigned int
型の変換std::stou
はありません.
なぜ用意されていないのかは不明ですが,簡単なので自作することができます.std::stou
は下記のようなコードで実装できます.
namespace std {
unsigned stou(std::string const &str, size_t *idx = 0, int base = 10) {
const unsigned long val = std::stoul(str, idx, base);
if (std::numeric_limits<unsigned>::max() < val) {
throw std::out_of_range("stou");
}
return static_cast<unsigned>(val);
}
} // namespace std
文字列から数値型に変換し,その成否を返す関数を作成する
std::stoi
は数値型への変換が失敗した場合,クラッシュするため例外処理をする必要があります.変換が失敗した場合にfalse
を返し,成功した場合,true
を返す関数を作成します.
bool ToValue(int &val, const std::string &str) {
try {
val = std::stoi(str);
return true;
} catch (...) {
return false;
}
}
文字列→数値型に変換する関数の速度比較
いくら安全だからといって,速度が遅ければ使い物になりませんので,速度検証を行います.
文字列→数値型の変換に対する速度を計測します.計測する対象は,
1. std::atoi
を使用する方法(推奨しない方法)
2. std::stoi
を使用する方法
3. ToValue
を使用する方法(今回作成した関数)
変換先の数値型および文字列の長さ(桁数)による影響を検証しました.また,最適化の有無による差も比較しました.
検証する文字列の長さはint
型の最大値2147483647(10桁)までの文字列を生成しました.
>> Optimized C++ ―最適化、高速化のためのプログラミングテクニック
int型への変換速度
最適化なし
最適化あり(-O2)
long型への変換速度
最適化なし
最適化あり(-O2)
double型への変換速度
最適化なし
最適化あり(-O2)
検証結果のまとめ
1.最適化なしの場合,std::atoi
が最も高速であり,2倍程度高速.
2.最適化を行った場合でも,std::atoi
が最も高速だが,std::stoi
およびToValue
に対して顕著な差はない.
3.int
およびlong
と比較するとdouble
への変換は低速である.
これらの結果から,使い勝手や速度を総合的に考慮すると,今回作成したToValue
関数を使用すると良いと思います.
検証に使用したコード
検証にはGoogle Benchmarkを使用しました.
Google Benchmarkに関してはこちらの記事に書いてありますのでご覧ください.
使用したコードはこちらです.
#include <benchmark/benchmark.h>
#include <algorithm>
#include <climits>
#include <cstring>
#include <string>
namespace std {
unsigned stou(std::string const &str, size_t *idx = 0, int base = 10) {
const unsigned long val = std::stoul(str, idx, base);
if (std::numeric_limits<unsigned>::max() < val) {
throw std::out_of_range("stou");
}
return static_cast<unsigned>(val);
}
} // namespace std
namespace {
bool ToValue(int &val, const std::string &str) {
try {
val = std::stoi(str);
return true;
} catch (...) {
return false;
}
}
bool ToValue(unsigned &val, const std::string &str) {
try {
val = std::stou(str);
return true;
} catch (...) {
return false;
}
}
bool ToValue(long &val, const std::string &str) {
try {
val = std::stol(str);
return true;
} catch (...) {
return false;
}
}
bool ToValue(unsigned long &val, const std::string &str) {
try {
val = std::stoul(str);
return true;
} catch (...) {
return false;
}
}
bool ToValue(long long &val, const std::string &str) {
try {
val = std::stol(str);
return true;
} catch (...) {
return false;
}
}
bool ToValue(unsigned long long &val, const std::string &str) {
try {
val = std::stoul(str);
return true;
} catch (...) {
return false;
}
}
bool ToValue(float &val, const std::string &str) {
try {
val = std::stof(str);
return true;
} catch (...) {
return false;
}
}
bool ToValue(double &val, const std::string &str) {
try {
val = std::stod(str);
return true;
} catch (...) {
return false;
}
}
static void BM_CharToInt(benchmark::State &state) {
const auto str_val = std::to_string(state.range(0)).c_str();
for (auto _ : state) {
auto val = std::atoi(str_val);
}
}
BENCHMARK(BM_CharToInt)
->RangeMultiplier(8)
->Range(8, std::numeric_limits<int>::max());
static void BM_StringToInt(benchmark::State &state) {
const auto str_val = std::to_string(state.range(0));
for (auto _ : state) {
auto val = std::stoi(str_val);
}
}
BENCHMARK(BM_StringToInt)
->RangeMultiplier(8)
->Range(8, std::numeric_limits<int>::max());
static void BM_StringToIntWithException(benchmark::State &state) {
const auto str_val = std::to_string(state.range(0));
for (auto _ : state) {
int val = 0;
ToValue(val, str_val);
}
}
BENCHMARK(BM_StringToIntWithException)
->RangeMultiplier(8)
->Range(8, std::numeric_limits<int>::max());
static void BM_CharToLong(benchmark::State &state) {
const auto str_val = std::to_string(state.range(0)).c_str();
for (auto _ : state) {
auto val = std::atol(str_val);
}
}
BENCHMARK(BM_CharToLong)
->RangeMultiplier(8)
->Range(8, std::numeric_limits<int>::max());
static void BM_StringToLong(benchmark::State &state) {
const auto str_val = std::to_string(state.range(0));
for (auto _ : state) {
auto val = std::stol(str_val);
}
}
BENCHMARK(BM_StringToLong)
->RangeMultiplier(8)
->Range(8, std::numeric_limits<int>::max());
static void BM_StringToLongWithException(benchmark::State &state) {
const auto str_val = std::to_string(state.range(0));
for (auto _ : state) {
long val = 0;
ToValue(val, str_val);
}
}
BENCHMARK(BM_StringToLongWithException)
->RangeMultiplier(8)
->Range(8, std::numeric_limits<int>::max());
static void BM_CharToDouble(benchmark::State &state) {
const auto str_val = std::to_string(state.range(0)).c_str();
for (auto _ : state) {
auto val = std::atof(str_val);
}
}
BENCHMARK(BM_CharToDouble)
->RangeMultiplier(8)
->Range(8, std::numeric_limits<int>::max());
static void BM_StringToDouble(benchmark::State &state) {
const auto str_val = std::to_string(state.range(0));
for (auto _ : state) {
auto val = std::stod(str_val);
}
}
BENCHMARK(BM_StringToDouble)
->RangeMultiplier(8)
->Range(8, std::numeric_limits<int>::max());
static void BM_StringToDoubleWithException(benchmark::State &state) {
const auto str_val = std::to_string(state.range(0));
for (auto _ : state) {
double val = 0;
ToValue(val, str_val);
}
}
BENCHMARK(BM_StringToDoubleWithException)
->RangeMultiplier(8)
->Range(8, std::numeric_limits<int>::max());
} // namespace
BENCHMARK_MAIN();
まとめ
文字列から数値型への変換に,std::atoi
を使用するのは控えましょう.
std::stoi
も有用ですが,今回作成した例外処理も考慮したToValue
関数を使用してみてはいかがでしょうか.