【C++】 文字列と数値の安全な相互変換と速度比較

私は普段から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::atoichar型をint型に変換するものでしたが,std::stoistd::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 → intstd::stoi
string → longstd::stol
string → long longstd::stoll
string → unsigned int定義されていない(後述)
string → unsigned longstd::stoul
string → unsigned long longstd::stoull
string → floatstd::stof
string → doublestd::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

short型への変換も標準では定義されていませんが,同様の方法で実装できます.

文字列から数値型に変換し,その成否を返す関数を作成する

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関数を使用してみてはいかがでしょうか.

よかったらシェアしてね!
目次