Modern C++ チャレンジ 読書ノート

〈C++17 プログラミング力を鍛える 100 問〉ということなので読み進める。

著者:

Marius Bancila

訳者:

黒川利明

技術監修:

島敏博

出版社:

オライリー・ジャパン

発行年:

2019 年

ISBN:

978-4-87311-869-7

まえがき

〈本書は、C++ 言語および標準ライブラリの機能だけでなく、多くのサードパーティのクロスプラットフォームのライブラリを練習できるように設計された実世界の問題を 100 問集めたものです。しかし、これらの問題で C++ 特有なのはわずかで、他のプログラミング言語でも解けるものです〉このことをよく覚えておくことだ。

〈本書の問題の回答のコードファイルは、GitHub と Packt のサポートページ(登録が必要)から入手できます〉とあり、前者の URL は次のようになっている:

<https://github.com/PacktPublishing/The-Modern-Cpp-Challenge>

ここで、上記リポジトリーをローカルディスクに clone して、ソースコードにコメントを付けていくスタイルで学習労力を省略化することにする。こうすると、どういうわけか本書が手許にないときでも学習を止めずに済む。

さらに、これらの問題の答案コードごとにすべてビルドすることにする。その過程で C++ 開発力のようなものも鍛えることを期待する。事実、かなり勉強させられた。

〈本書の解答全てがクロスプラットフォームですので、どのプラットフォームでも動作します〉とある。私は WSL 環境でビルドをしていく。

1 章 数学の問題

全体的に auto, decltype, constexpr, noexcept を付加する余地がある関数が多い。例えば解答 4 の関数 is_primeconstexpr の他に noexcept も付加できる。

  • 大きな上限まで加算するために long long を使う (p. 3)

  • 解答 1 の for ループのカウンター宣言に decltype(limit) としたい。

  • C++17 には <numeric>std::gcd() がある (p. 4)

  • 解答 2 の自作関数は constexpr 宣言できる。

  • C++17 には <numeric>std::lcm() がある (p. 5)

template<class InputIt>
constexpr int lcmr(InputIt first, InputIt last) noexcept
{
    // C++ 17 では <numeric> に std::lcm() がある (p. 5) が、
    // 次のようにテンプレート引数を明示しないと g++ 9.3.0 はエラーを出す。
    using IntType = typename InputIt::value_type;
    return std::accumulate(first, last, 1, std::lcm<IntType, IntType>);
}
  • C++17 ならば if 文の変数スコープも細かく直そうと思えば直せる。

    if (auto sum1 = sum_proper_divisors(number); sum1 < limit)
    {
        // code
    }
    
  • 解答 8 でラムダ式登場。

  • 解答 8 のクラステンプレート perf_timer は難しい要素が多い。 std::invoke() を使うには <functional> をインクルードする必要がある。

  • 本書では修正されているところが多いが cbegin(), cend() が使えるアルゴリズム呼び出しが多い。

  • 解答 9 改変例

    auto prime_factors(unsigned long long n) -> std::vector<decltype(n)>
    {
        // code
    }
    
  • 解答 10 で範囲 for 文初登場。

  • 解答 12 のコードを私が書き直すと decltype(limit) があちこちに出る。これは良くなるか?

  • 解答 13 で乱数登場。

  • 地味なので std::ref() は付け忘れそうだ。

2 章 言語機能

可能な限り begin(), end()cbegin(), cend() にそれぞれ置き換える。

  • クラステンプレート std::array を利用できないか意識する。

    • この配列型は要素次第の型次第でコンストラクターやコピー操作が noexcept に指定できる。

  • コンストラクターに対しても constexpr をできないか意識する。

  • コンストラクター呼び出しは中括弧のほうが利用頻度が高くなりそうだ。

  • 解答 17 はいろいろと加筆できる。例えば cbegin(), cend() を実装するとか。 main() の最後で std::copy() を呼び出すところでこれらを利用したい。

  • 解答 18 でパラメーターパックが出る。ちなみに std::min() が本問の要求の本質的に満たす:

    std::cout << std::min({5, 4, 2, 3}) << std::endl;
    std::cout << std::min({3, 2, 1, 0}, std::less<>()) << std::endl;
    
  • 解答 19 では畳み込みを習う。このコードは覚えにくい。

    • 実際には .insert() を使うといい?

  • 解答 20 でも畳み込み。短絡評価が効くことを覚えておくこと。

    • パラメーターパックと組み合わさった T&& に注意。特に contains_none() の実装で std::forward が出てくることを意識する。

  • 解答 21 は Windows 専用のように見えるが WSL でも実行はできる。というか、ダミーの if 文によりドライバー関数が終了する。

    • std::runtime_error のために <stdexcept> をインクルードする。

    • RAII クラスではコンパイラーが生成するデフォルトコンストラクターとコピー代入演算子を禁止するのがよい。

    • メンバー関数 release()std::exchange() が使えそうだ。

    • std::vector<char> buffer(1024)std::array<char, 1024> に置き換えることもできる。

  • 解答 22 でリテラル演算子の定義を学べる。このコードの関数群は特に noexcept を付けられるものが多い。

3 章 文字列と正規表現

  • std::string_view を使いこなせるようにすること。関数の引数リストに書くときには値渡しとする?

  • 解答 25 の冒頭は別名テンプレートという機能か。

  • using namespace std::string_literals; と宣言する。これによりリテラル文字列の suffix に s を付けると std::basic_string オブジェクトであるとして扱われる。

  • 解答 27 で関数が inline 宣言されているが、これは特に深い意味はなさそうだ。

  • 解答 28 の関数 longest_palindrome の最後で std::string のコンストラクターが必要な理由はオブジェクト str.substr() がビューだからだ。この関数の戻り値を変えれば、あるいは……。

  • decltype(x)x から cv を外したい場合はどうするか?

  • std::regex を使いこなせるようにすること。Python とほとんど変わらない感覚で書けるか。

  • C++ の生文字列は若干タイプしづらい。ダブルクォーテーションの隣に丸括弧が必要だ。

  • (*i)[1].matched みたいな書き方しかできないか?

  • 解答 30 で parse_uri() の引数を std::string から std::string_view に置き換えることを考える。そのとき次が必要:

    • std::smatchstd::cmatch に変える。

    • std::regex_match() へ渡す実引数 uriuri.data() に変える。

  • std::stoi() 系の関数を使いこなせるようにすること。

  • return {}

4 章 ストリームとファイルシステム

  • 解答 32 の Pascal の三角形。ストリーム要素もファイルシステム要素も目新しさはない。

  • 解答 33 を見て思う。enum class は文字列表示機能がないのか。

  • std::filesystem は新しいライブラリーだ。

  • std::uintmax_t なる型が <numeric> に定義されている。

  • 解答 36 ファイル更新時刻を取り扱う。

    • ファイルの最終更新時刻を取得するのは std::filesystem::last_write_time()

    • 現在の時刻を取得するのは std::chrono::system_clock::now()

  • std::filesystem::recursive_directory_iterator() のパスをたどる基準のようなものは?

  • WSL 環境では、解答 38 をコンパイルするのに apt install uuid-dev を必要とする。さらに memcpy が宣言されていないとエラーが出るはずなので、 uuid.h をインクルードする前に <cstring> をインクルードする必要がある。

  • 解答 38 の logger::~logger() は例外を握りつぶしているマナーの悪いコードに見えるかもしれないが、妥当だ。

5 章 日付と時間

  • std::invoke() を使うのなら <functional> をインクルードする必要がある。

  • 解答 39 のクラステンプレート perf_timer で完全転送の使い方を学ぶ。

  • std::this_thread::sleep_for()

  • 解答 40, 41 で使っているライブラリー

    • 丸括弧キャストを static_cast に書き換え可能

  • 解答 43 は curlcpp をリンクする必要がある。

    bash$ ./build/problem_43
    Hour:23
    Minutes:16
    Local time:    2020-12-28 23:16:00 JST
    Ildiko         2020-12-28 15:16:00 CET
    Jens           2020-12-28 15:16:00 CET
    Jane           2020-12-28 09:16:00 EST
    

6 章 アルゴリズムとデータ構造

この章で扱う標準アルゴリズムは反復子を入れ替えるものが多いことに注意する。それでもなお、GitHub のコードには cbegin(), cend() で置き換える余地のあるものが残されている。

ラムダ式を grep するのがたいへん難しい。けっこう困る。

  • std::push_heap(), std::pop_heap() には cbegin(), cend() を渡せないことは理解している。

  • 解答 45 .top()noexcept のはず。

  • ここのフリー関数テンプレート swap()noexcept() の付け方をよく理解すること。

  • 解答 46 のリングバッファーの反復子のカテゴリーをランダムアクセス反復子とするのは違和感がある。

  • pop() が値を返すのは微妙な設計なのではなかったか。

  • 解答 47 のダブルバッファー実装は理屈だけでも理解しておくこと。特に、mutex の使い方は基本的なので外さないこと。

  • 解答 53 の truncated_mean() の途中で rbegin() も使える。

  • そういえばラムダ式の引数リストの型には auto が許されるのか。

  • 解答 56 の最初の select() は難しい。

  • 解答 57 の print() はランダムアクセス反復子である必要はまったくなく、 ++ さえ機能する反復子なら十分だ。

  • 解答 58 は Boost.Graph の dijkstra_shortest_path() のようなものを作る。

  • 解答 59 のラムダ式、先述のとおり auto と書ける。以下同様。

  • 解答 60 の cell()const 版も欲しい。そうすることでいくつかのメンバー関数も const にできる。

7 章 並行処理

この章の問題はすでに標準ライブラリーが提供している機能を求めるものがある。その確認も行うこと。

WSL でビルドする場合には、コンパイルオプション -pthread を追加することが必要となる。

  • 解答 61 にも std::forward() の用例がある。

  • std::transform() の第一範囲は const_iterator を指定するのが丁寧だ。

  • 解答 62 のスレッドのコンテナに .emplace_back() でラムダ式を追加している。キャプチャーリストに注意。

  • 解答 63 は std::future のコンテナを取り扱う。

    • typename std::iterator_traits<Iterator>::value_type が二度出てくる。 using で別名を定義するべきだろう。

  • 解答 64 は解答 57 の変種。クイックソートは並列化のいい練習問題だ。

  • これまでも何度か目にしたが std::generate() 系アルゴリズムの応用がうまい。

  • 解答 65 のクラス logger は Singleton デザインパターンの現代風の実装を教えてくれる。

  • std::to_string()

  • 解答 66 は Consumers/Producers パターン。 std::condition_variable の連携がわかりにくい。

8 章 デザインパターン

デザインパターンは基本的なのでしっかり見ていく。

解答 67 はパスワードの検証ということで Decorator パターンを適用している。

auto validator = std::make_unique<symbol_password_validator>(
    std::make_unique<case_password_validator>(
        std::make_unique<digit_password_validator>(
            std::make_unique<length_validator>(8))));
  • パターンとは関係ないが、継承ツリー最下層のクラスを final 宣言する。

  • これも関係ないが、オーバーライドメンバー関数を明示的に override 宣言する。

  • 装飾されるオブジェクトを std::unique_ptr で持つ。これは値渡しとする。その際コンストラクターで std::move() を併用する。

  • std::unique_ptrstd::make_unique() で生成するのがよい。

解答 68 は Composite パターンを適用して、パスワードをランダムに生成する。前項の Decorator パターンと同様に std::unique_ptr を駆使するのがコツとなる。状況によって std::shared_ptr になることもあるだろう。

composite_password_generator generator;
generator.add(std::make_unique<symbol_generator>(2));
generator.add(std::make_unique<digit_generator>(2));
generator.add(std::make_unique<upper_letter_generator>(2));
generator.add(std::make_unique<lower_letter_generator>(4));

auto password = generator.generate();

解答 69 は Template Method パターン。基本的なパターンゆえにモダンも何もない気がする。

  • 抽象基底クラスの純粋仮想関数に noexcept と書くのは度胸が要る。

  • 乱数生成コードは毎回 5 行くらいの決まり切ったものになるので、VS Code などの snippets として定義しておくのがいいだろう。

  • メンバー関数 next_random() で出来合いの乱数器から乱数を得る。

解答 70 は Chain of Responsibility パターン。メンバー関数 approve() を見ればわかるだろう。従業員が自分の一存で扱える金額ならば経費を処理し、そうでなければ直属の上役の決裁を仰ぐ。

  • std::numeric_limits<double>::max() は覚えておこう。

解答 71 は Observer パターン。

  • 冒頭の to_string() は C++ 言語でサポートしてくれないか。

  • [[nodiscard]] が付いているメンバー関数がある。余計なおせっかいという気もする。

  • void push_back(T&&) は universal reference ではなくて、ふつうの右辺値参照引数だ。テンプレート引数は確定している。

解答 72 は Strategy パターン。値引額を決定するという、どこかで見た問題設定だ。

9 章 データシリアライゼーション

この章からは非標準ライブラリーを利用する解答が多い。 C++ によるプログラミング能力だけではなく、そのようなライブラリーをビルド、リンクする能力も備えろ。

ただし、これ以降に登場するような問題は Python で書いたほうがいいと思う。あとで各問題の Python による解答を用意するのも面白いだろう。時間があればやりたい。

XML ファイルのシリアライズには pugixml というライブラリーを採用。これはソースファイルが一つしかないので main.cpp と同時にコンパイルすればいい。解答 73 と74 の CMakeLists.txt を見るとそのようにしている。

JSON ファイルのシリアライズには nlohmann/json を採用。構文が直観的にわかりやすいそうだ。これはヘッダーファイルしかないライブラリーなので、ビルドをしなくて済む。

  • 解答 75 の関数 to_json()main.cpp からは呼び出されていないが、 nlohmann::json 内のシリアライズ機能が参照する。コメントアウトしてはならない。

PDF ファイルのシリアライズには PDF-Writer を利用する。 Python のときもそうだが、PDF の問題はフォントの設定で困ることが多い。

PDF-Writer のビルドを別途する必要がある。このディレクトリーに移動して cmake 作業をする。このビルドの出力先を変える場合、解答 77 の CMakeLists.txt も編集する必要があるかもしれない。

10 章 アーカイブ、画像、データベース

こういうプログラムを C++ で書くのは勘弁願いたいものだ。

  • 解答 79 のコードを見るに、ZipLib のインターフェイスは洗練されているとは言えないようだ。しかもヘッダーファイルをインクルードするとコンパイラーが警告を多数出す。

  • 範囲 for 文のコロンの右側で関数呼び出しをしても一度しか呼ばれないで済むようだ。

  • 解答 80 で std::function の利用例を見られる。コールバックとして利用している。

  • 解答 83 のフォント周りの処理は Linux 専用コードを書く必要がある。

  • 解答 85, 86 の関数 get_directors() 内などのラムダ式の引数リストでは auto が許されない。

  • 解答 87 の reinterpret_cast はよろしくない。&data[0] が正解。

  • std::stoi()

11 章 暗号

C++ 新機能の学習からは離れていく。最初の 2, 3 問は標準ライブラリーしか利用していないので気が楽だ。

  • 解答 90 は unsigned の切り替えが何なのかよくわからない。

  • 範囲を引数に取るコンストラクターを使って勝手に書き直す:

    auto from_string(std::string_view data)
    {
        return std::vector<unsigned char>(std::cbegin(data), std::cend(data));
    }
    
    auto from_range(std::vector<unsigned char> const & data)
    {
        return std::string(std::cbegin(data), std::cend(data));
    }
    
  • 解答 91 以降で Crypto++ というライブラリーを利用する。 CMakeLists.txt の静的リンクライブラリーファイル名がミスっているので修正する。

  • 解答 91 の reinterpret_castchar*unsigned char* にキャストする。

  • std::filesystem::path::string()

  • 解答 94 については本書の説明を読むこと。わかりやすさのために無駄なことをしている。

12 章 ネットワークとサービス

C++ のコードとして面白いかというとそんなことはない。

  • Asio はヘッダーファイルからなるライブラリーだ。

  • 解答 96 の解答は 2 ディレクトリーに分かれている。サーバー側をバックグランドで起動すると良い。

  • ソケットプログラミングにおける std::array の存在感のしっくりさ。

  • std::enable_shared_from_this は説明を要する。これを継承するサブクラスは、メンバー関数内で shared_from_this() を呼び出すことによりサブクラス自身の std::shared_ptr オブジェクトを得る。このコードで言うとメンバー関数 read() の冒頭でそれを確認できる。

  • std::error_code

curl 系ライブラリーのビルドにはひじょうに苦労させられた。

  • 解答 98 のコードを使って自分の Gmail のアカウントにアクセスしてみたが失敗した。

  • 解答 99, 100 のコードは Microsoft Azure の各種サービスを使う。アカウントを sign up しておく必要がある。面倒なのでやっていない。

  • 解答 99 はエンコーディング変換関数の実装例。

    • 最後の for ループは初めて見るタイプの構文だ。

  • uint8_t

付録 A 参考文献

すべての論文・ライブラリーについて URL が併記されている。

訳者あとがき

〈「モダン」な解法の難しいところは、外部のライブラリや API に依存するところです〉とある。そのためかどうか知らないが、本書で印刷されているコードと GitHub のコードとで細かい差異が多数あった。それを確認するのもいい勉強になった。

その外部のライブラリをビルドする手順は本書ではほとんど記されていないので、著者が確認した環境以外で試そうとすると、ほんとうに challenge になる。

追加的作業

CMakeLists.txt を検証する

自然なビルド手順は、トップレベルにある CMakeLists.txt を参照した cmake コマンドを実行することで各解答に対応するサブディレクトリーの CMakeLists.txt により main.cpp が適宜コンパイル、リンクされて、実行形式がトップ直下の bin サブディレクトリーに生成されるというものだろう。

WSL (Ubuntu) 環境でこのビルドを実行すると失敗する。原因は大きく分けて二つある。まず、サードパーティー製ライブラリーの一部が cmake でビルドできないことだ。

  • Crypto++ は CMakeLists.txt が全然作り込まれていないことから、これを使うとビルドできないのは想像に難くない。GNUmakefile があるということは make しろということだろう。事実、時間はかかるがそれでモノが生成する。特定のソースコードのコンパイルに長時間かかる。

  • cURL はビルド手順を忘れた。いつの間にかなんとかなっていた。

  • cURL と curlcpp のライブラリーファイルはトップレベル直下の bin/libs には来ない?

それから、解答コードの一部がビルドに失敗することだ。著者は Windows と Mac で動作確認をしたとあり、 Linux についてはビルドが成功することを保証しているわけではない。

  • main.cpp で、インクルードが不足しているものがある。これは修正が容易だし、C++ の学習にもなるので不問にしてよい。

  • マルチスレッド関連の解答ではコンパイルオプション -pthread あるいは同等のオプションの明示的追加が必要。

  • include_directories(${LIBS_PATH}/stduuid)

  • リンクするライブラリー名が他の環境と異なっている?

    #add_library (cryptlib STATIC ${headers} ${sources})
    add_library (cryptopp STATIC ${headers} ${sources})
    
  • リンク順がビルドの結果に影響する可能性がある。気のせいかも知れない。

    target_link_libraries(problem_97 curl curlcpp)