📝

ちょっと便利な文字列変換の紹介

に公開

今回紹介するのは、文字列を数字に変換する関数です!
「そんなもん誰でも知っとるわい!std::stoiでしょ!」と思っているあなた!
私も昔からstd::stoiを使っていますが、他にもstd::atoi, std::stringstream, sscanf などという選択肢もあります。
文字列に数字しか入っていないという保証がある場合は、正直どちらを使っても問題ないです。

std::string str = "12345";
int number = 0;

number = std::stoi(str);

std::atoi(str.c_str());

std::stringstream strm(str);
strm >> number;

sscanf(str.c_str(), "%d", &number);

使い方がそれぞれ違うため、好きな方を使えばいいです。

  • 私は可能な限りモダン C++ のスタイルで行きたい派なので、sscanfは個人的に論外です。
  • std::atoiconst char*を渡さないといけないため、少し使いづらいです。
  • std::stringstreamはファイルやユーザーインプットからの読み込みの場合は使うと思いますが、特定の文字列を変換するためだけには少しやりすぎな気がします。
  • 結局std::stoiになるのですが、実はstd::stoiが使えない環境も存在します。これについては後ほど説明します。

さっきの例は数字しか入っていない文字列を変換しましたが、数字でない文字が入っている場合はどうしますか?

std::atoiの場合は、文字列が数字でない場合は0を返します。そうすると、「インプットがたまたま "0" という文字列だった場合」と区別できないため、お勧めできないです。

それに対して、std::stoiでは以下のように対応すると思います。

std::size_t pos{};
try
{
    number = std::stoi(str, &pos);
    std::cout << "number: " << number << std::endl;
}
catch (std::invalid_argument const& ex)
{
    std::cout << "std::invalid_argument::what(): " << ex.what() << std::endl;
}
catch (std::out_of_range const& ex)
{
    std::cout << "std::out_of_range::what(): " << ex.what() << std::endl;
}

if (pos != str.length())
{
    std::cout << "conversion was partly successful" << std::endl;
}
else
{
    std::cout << "conversion was successful" << std::endl;
}

文字列に数字でない文字しか入っていないせいで変換に失敗したら、std::invalid_argumentという例外が投げられます。
数字への変換が成功したがint型に収まらないぐらい大きな数字だった場合は、std::out_of_rangeという例外が投げられます。
そういう時はstd::stolというlong型版かstd::stollというlong long型版を使うといいです。

ただ、-fno-exceptionsという例外が投げられないビルド設定が採用されている環境も存在します。そういう環境ではstd::stoiの例外に対応できないため、文字列の変換が失敗しかねない場合は使えません。

そこで今回紹介したい関数std::from_charsの出番です!

const auto last = str.data() + str.size();
auto [ptr, ec] = std::from_chars(str.data(), last, number);

if (ec == std::errc())
    std::cout << "number: " << number << std::endl;
else if (ec == std::errc::invalid_argument)
    std::cout << "This is not a number." << std::endl;
else if (ec == std::errc::result_out_of_range)
    std::cout << "This number is larger than an int." << std::endl;
else if (ptr != last)
    std::cout << "conversion was partly successful" << std::endl;
else
    std::cout << "conversion was successful" << std::endl;

数字でない文字ばっかり入っているせいで変換に失敗したら、ecstd::errc::invalid_argumentというエラーが返されます。
数字への変換が成功したがint型に収まらないぐらい大きな数字だったら、std::errc::result_out_of_rangeというエラーが返されます。

基本的にstd::stoiのような使い方ですが、個人的に少し使いづらいと思うのは、numberが出力引数になっていることです。本当はptrecと一緒に戻り値として返して欲しいところです。
引数がconst char*になっているのも少し微妙です。でも例外に対応できない環境でも使えるのでお勧めです!

補足説明として少しだけ触れますが、std::stoistd::from_charsにはbaseという基数が指定できる引数も存在します。どちらもデフォルト値が10進数の10になっていますが、2から36が指定できます。

std::string hex_str = "ff";
int hex_val;
std::from_chars(hex_str.data(), hex_str.data() + hex_str.size(), hex_val, 16);
std::cout << "Parsed hex: " << hex_val << std::endl; // Output: 255

std::string bin_str = "1010";
int bin_val;
std::from_chars(bin_str.data(), bin_str.data() + bin_str.size(), bin_val, 2);
std::cout << "Parsed binary: " << bin_val << std::endl; // Output: 10

std::stoiの場合は、base0も指定できます。そうすると、std::stoiが以下のルールで自動的に基数を推測してくれます。

  • 文字列が0から始まる場合は8進数として解釈される
  • 文字列が0xから始まる場合は16進数として解釈される
  • それ以外は10進数として解釈される

それに対して、std::from_charsbase0を指定すると、ecstd::errc::invalid_argumentが返ります。


|cpp記事一覧へのリンク|

Discussion