何だかんだでC++を書き続けています。今やC++20です。C++0xで騒いでいた頃が懐かしいです。
今回は最近自分がC++を書く時に気にしていることについて簡単に紹介していこうと思います。
基本方針
同じコードを触る開発者が多いため、実装者にとっての利便性よりも共同開発者にとっての可読性や保全性を重視しています。そのため、可読性を下げる機能や扱いが難しい機能は扱わない事が多いです。
auto
autoは書き手にとっては便利ですが、多用すると(特に共同開発者にとっての)コードの可読性が下がるためあまり使いません。
std::optional<std::vector<CrawledArticle>> articles = CrawlArticles(...);
auto articles = CrawlArticles();
ただし、コンテナのイテレーターは型名が無駄に長く、また変数の型が周辺のコードから明らかなことが多いためautoを使っています。
同様の理由で、std::make_uniqueの戻り値を受ける変数に対してもautoを使っています。
auto crawler = std::make_unique<ArticleCrawler>(...);
std::unique_ptr<ArticleCrawler> crawler = std::make_unique<ArticleCrawler>(...);
std::pairとstd::tuple
operator<などが定義されているため便利ですが、first、secondやget<3>はdescriptiveな名前ではなく、可読性が下がるため極力使わないようにしています。可能であればstructを定義した方が可読性が高いコードになります。
if (ProcessArticle(article_info.second)) {...}
if (ProcessArticle(std::get<2>(article_info))) {...}
継承
ポリモーフィズムのための継承はほぼ使っていません。たまにStrategyパターンのために使うことがある程度ですが、多用すると他の開発者にとっての可読性が下がる(オブジェクトがインスタンス化されている箇所まで辿らないと仮想関数呼び出しがどの実装を呼んでいるのか分からなくなる)ため気をつけています。Template Methodパターン等、実装を共有する目的での継承は使っていません(共同開発者で使っている人はいます)。
Dependency Injectionでユニットテストをしやすくする目的での継承は使っています。Abstract Factoryも稀に使います。ただし、具象クラスを使ったテストで済む場合(ネットワークやディスクアクセスを起こさず、動作も十分速いクラスに依存するコードの場合)はなるべくDependency Injectionを使わずにテストしています。
テンプレート
テンプレートは強力ですが、共同開発者がコードを読む時に関数テンプレートやクラステンプレートがインスタンス化されている箇所まで辿らないと動作の確認ができない場合があるため多用しないようにしています。ただし、他の人も色々な型の変数で使いたくなるようなライブラリを設計する場合は話が変わってくると思います。
std::unique_ptr
非常によく使います。生のポインタをnewすることはほぼありません。std::unique_ptrをインスタンス化する時はstd::make_uniqueで行います。std::unique_ptrのコンストラクタはほぼ使いません。
std::unique_ptrは関数のシグネチャー上でオブジェクトの所有権の移動を明確化するためにも使えます。可読性や保全性の向上が期待できます。
class Notifier {
public:
explicit Notifier(std::unique_ptr<MailSender> mail_sender)
: mail_sender_(std::move(mail_sender)) {}
...
private:
std::unique_ptr<MailSender> mail_sender_;
};
...
auto mail_sender = std::make_unique<MailSender>(...);
...
auto notifier = std::make_unique<Notifier>(std:move(mail_sender));
std::unique_ptr<ArticleCrawler> CreateArticleCrawler();
ちなみにstd::shared_ptrは今のところ滅多に使っていません。
Designated initializers
structの初期化で便利かつ可読性も向上するのでよく使います。日本語では指示付き初期化と言うらしいです。
struct Person {
std::string name;
int age = 0;
};
Person person = {.name = "John Doe", .age = 42};
指示付き初期化は元々C99で導入された機能ですが、C++20でも利用可能になりました。ただしいくつかC++20特有の制約があります(こちらのページの"仕様"を参照してください)。
他にも、指示付き初期化は引数が多いorデフォルト引数の一部だけを変えて呼び出すことが多い関数に使うと便利です。
abseil / Tip of the Week #173: Wrapping Arguments in Option Structsに載っている例を少し簡略化したものを以下に紹介します。
struct PrintDoubleOptions {
std::string_view prefix = "";
int precision = 8;
char thousands_separator = ',';
char decimal_separator = '.';
bool scientific = false;
};
void PrintDouble(double value,
const PrintDoubleOptions& options = {});
PrintDouble(5.0, {.prefix = "value=", .scientific = true});
structと指示付き初期化なしだと以下のように扱いづらいコードになります。
void PrintDouble(double value, std::string_view prefix = "", int precision = 8,
char thousands_separator = ',', char decimal_separator = '.',
bool scientific = false);
PrintDouble(5.0, "value=", 8, ',', '.', true);
終わりに
他にもプロジェクト独自の規約や自分なりの基準がありますが、最近よく気にしているのはだいたいこの辺りです。