メモの日々


2018年02月15日(木) [長年日記]

[c++] C++で日本語を扱う

C++での日本語の扱い方をよくわかっていなかったのでメモ。

言語処理100本ノックにある次の問題をC++14で解いてみる。

01. 「パタトクカシーー」

「パタトクカシーー」という文字列の1,3,5,7文字目を取り出して連結した文字列を得よ.

std::wstringを使ってみる

std::stringから日本語のようなマルチバイト文字列のn文字目を取り出すのは簡単ではない。ここはstd::wstringの出番か。

#include <iostream>
#include <numeric>
#include <string>

// sからindexesで指定された位置の文字を繋いだ文字列を返す。
std::wstring select(
    const std::wstring& s,
    std::initializer_list<std::size_t> indexes)
{
  return std::accumulate(
      indexes.begin(),
      indexes.end(),
      std::wstring{},
      [&s](auto& result, auto i) { return result + s.at(i); });
}

int main()
{
  std::ios_base::sync_with_stdio(false);
  std::wcout.imbue(std::locale(""));
  std::wcout << select(L"パタトクカシーー", {1, 3, 5, 7}) << std::endl;
}
  • wstring::at()を使えばn文字目を簡単に取り出すことができる。
  • wstringをそのまま出力するにはstd::wcoutを使う必要がある。
  • しかし、wcoutをそのまま使っても正しく出力されない。basic_ios::imbue()を呼び出してストリームにロケールを設定しなければならない。
  • ロケールを設定するだけではまた正しく出力されない。ios_base::sync_with_stdio()を呼び出してCの標準入出力関数との同期をオフにする必要があった。
  • なお、sync_with_stdio()とimbue()呼び出しの代わりにstd::setlocale()を呼び出す手もある。

std::stringだけで処理する

wstringは一見お手軽なのだけれど、外部とのやり取りが発生すると面倒なことになりそうなのでなるべく使いたくない。std::stringだけで処理するにはどうするか。

マルチバイト文字列から1文字を取り出すには<cstdlib>にあるstd::mblen()が使えそう。これを使うと、長くなるけど次のように実装できる。

#include <cstdlib>
#include <iostream>
#include <iterator>
#include <numeric>
#include <stdexcept>
#include <string>

// マルチバイト文字列sのindex番目のマルチバイト文字を表す文字列を返す。
std::string get_mb_char(const std::string& s, std::size_t index)
{
  auto first = s.begin();
  std::size_t len = 0;
  for (std::size_t i = 0; i <= index; ++i) {
    first += len;
    if (first == s.end()) throw std::out_of_range("invalid index.");
    len = std::mblen(&*first, std::distance(first, s.end()));
    if (len < 0) throw std::runtime_error("invalid string.");
  }
  return {first, first + len};
}

// sからindexesで指定された位置のマルチバイト文字を繋いだ文字列を返す。
std::string select(
    const std::string& s,
    std::initializer_list<std::size_t> indexes)
{
  return std::accumulate(
      indexes.begin(),
      indexes.end(),
      std::string{},
      [&s](auto& result, auto i) { return result + get_mb_char(s, i); });
}

int main()
{
  std::setlocale(LC_CTYPE, "");
  std::cout << select("パタトクカシーー", {1, 3, 5, 7}) << std::endl;
}
  • mblen()を使うにはstd::setlocale()によるロケールの設定が必要。
  • n文字目を取り出すには文字列の先頭から順番に調べていく必要があるので効率が悪い。

2018年02月16日(金) [長年日記]

[c++] C++の可変引数テンプレートを使いこなせない

C++の可変引数テンプレートの使い方を覚えられないのでメモ。

昨日のコードでは可変長の引数を受け取るのに initializer_list を使ったが、可変引数テンプレートを使えば呼び出し側の

select(L"パタトクカシーー", {1, 3, 5, 7})

にある中括弧を無くせる。一応次のように書けるのだけれど、少し問題がある。

#include <iostream>
#include <numeric>
#include <string>

// sからindexesで指定された位置の文字を繋いだ文字列を返す。
template<typename... Args>
auto select(const std::wstring& s, Args... indexes)
-> decltype(std::initializer_list<int>({indexes...}), std::wstring{})
{
  std::initializer_list<int> is{indexes...};
  return std::accumulate(
      is.begin(),
      is.end(),
      std::wstring{},
      [&s](auto& result, auto i) { return result + s.at(i); });
}

int main()
{
  std::ios_base::sync_with_stdio(false);
  std::wcout.imbue(std::locale(""));
  std::wcout << select(L"パタトクカシーー", 1, 3, 5, 7) << std::endl;
}
  • C++ で特定の型での可変長引数を受け取るの真似をして、decltypeを使ってindexesをint列に変換できることをチェックしている。このチェックがなくても動くが、indexesに誤った型の値が設定されたときのエラーメッセージが分かりやすくなる(気がする)。

問題点は、昨日のコードはselect()の引数に負の数を指定したときにコンパイルエラーになったのに、上のコードはならないこと。initializer_list<> に与える型を int ではなく size_t にすれば解決するかと思ったが、それだと呼び出し側を

select(L"パタトクカシーー", 1u, 3u, 5u, 7u)

のように書かないとコンパイルエラーになるようになってしまう。

可変引数テンプレートはなかなか使いこなせるようにならない。