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_prime
は constexpr
の他に
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::smatch
をstd::cmatch
に変える。std::regex_match()
へ渡す実引数uri
をuri.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_ptr
をstd::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_cast
はchar*
を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)