Rossamu++

オールラウンダーになりたいと思っている僕っ娘の何の変哲もないブログ。

つまづいたっていいじゃないか、C++だもの

これはC++ Advent Calendar 2015 - Adventarの19日目の記事です。

どうも、ろっさむです。
本日二つ目の記事ですね、流石に少しだけ疲れましたが
僕はまだまだ元気です。0x15歳だしネ。

さて、今回のテーマは

C++初心者でも使える3つのテクニック」

となりました。
対象は中級者という枠組みに片足の先っぽを突っ込んでる方です。そう、僕みたいな。
メタプログラミングについて書く予定でしたが
プログラミング言語C++第4版」を読んで勉強しているうちに
色々書きたくなったので変更です。すいません。

紹介するテクニックは以下の通りです。

  1. インタフェースと実装の名前空間は分離しよう。そして平均的ユーザ用インタフェースと熟練者用インタフェースも区別しよう。
  2. 返すべき結果が大規模であればムーブコンストラクタを利用しよう。
  3. 大規模テンプレートとある程度以上文脈に依存するテンプレートは分割してコンパイルしよう。

それではいってみヨーカドー。


1.名前空間を用いたテクニック手法

これから極端な例を示します。

例えば今、貴方が何らかのシステム開発に携わっていたとします。
既存のソースコードを使用しつつ追加機能を開発しなければいけない場合に、
「あの機能を使いたいんだけど、どの関数を呼び出せばいいんだ?」と困ってしまいました。
使いたい関数が恐らく含まれている名前空間を見つけましたが、
複数の関数が以下のように記述されています。

namespace Parser{
    double prim(bool);
    double term(bool);
    double expr(bool);
}

それぞれの関数の中身を見てみると、
必要となるのユーザ用の機能はどうやらexpr()らしいぞい…。

これはいけません。
ユーザに示すインタフェースはもっと単純である必要があります。
Parser名前空間を次の二つを提供するように改良しましょう。

  • Parserを実装する関数用の共通環境
  • Parserのユーザに提供する外部インタフェース
//parser.h
//ユーザインタフェース
namespace Parser{
    double expr(bool get);
}

//実装者用インタフェース
namespace Parser{
    double prim(bool get);
    double term(bool get);
}

これで少しは改良されました。
ユーザ用インタフェースと実装者用インタフェースとで
名前を変えることも可能です。

//parser.h
//ユーザインタフェース
namespace Parser{
    double expr(bool get);
}

//実装者用インタフェース
namespace Parser_impl{
    using namespace Parser;

    double prim(bool get);
    double term(bool get);
}

_implを付けることで実装者用インタフェースを
わかりやすくしました。
これは大規模プログラムの場合によく用いられます。

しかしプログラムの物理的な構成が
そのままファイルで分割した名前を提供するので
必ずしもこうする必要はありません。

//parser.h
//ユーザインタフェース
namespace Parser{
    double expr(bool get);
}
//parser_impl.h
#include "parser.h"

//実装者用インタフェース
namespace Parser{
    double prim(bool get);
    double term(bool get);
    double expr(bool get);
}
#include "parser_impl.h"

double Parser::prim(bool get) {/*...*/}
double Parser::term(bool get) {/*...*/}
double Parser::expr(bool get) {/*...*/}

これで完了です。
この構造は大規模なモジュールでなければ不適切です。
現実的規模のモジュールでは個々の関数が必要とするファイルを
#includeするのが一般的です。

この構成を利用する最大の理由は、
プログラミングの際に考慮するべき部分を局所化できることです。
複数ヘッダ構成だと「◯◯のコードが何に依存しているか」を
正確に把握しやすくなり、それ以外のプログラム部分を無視できます。
局所化がうまくいくと、モジュールのコンパイルに必要な情報量が削減でき
短時間でコンパイルできるようになります。(1/1000になる可能性も)

また、実装者用インタフェースとユーザ用インタフェースを区別することによって
一般ユーザと熟練者用ユーザのそれぞれに丁度良い機能を提供することができます。
例えば一般ユーザには簡素化されたインタフェースを、
熟練者用ユーザには拡張されたインタフェースを提供できます。
DirectX(熟練者用)とDXライブラリ(一般用)みたいな感じですね。

単一ヘッダ方式と複数ヘッダ方式のどちらにするかの選択は
プログラムを構成するパーツの局所局所で変わってきます。
また、単一ヘッダ方式と複数ヘッダ方式は二社択一の性質ではなく、
システムの進化とともに見直すことになる相互補完的性質のテクニックです。


2.大きい返却値はムーブで

引数には二つの選択肢があります。
皆さんご存知の通り、「値渡し」と「参照渡し」ですね。
4ワード以下の小規模なオブジェクトなら値渡しが最高の性能を得られます。
しかし、引数の受渡しと利用の性能は
マシンアーキテクチャコンパイラのインタフェース規約、
引数がアクセスされる回数(参照渡し引数よりも値渡し引数への
アクセスのほうが高速である場合が殆どである)に依存します。

例えばMatrixの場合は大規模オブジェクトの場合は参照渡しの方が良いです。

//const参照渡し
Matrix operator+(const Matrix&, const Matrix&);

多くの場合、演算子は何らかの結果を返却します。
新しく作ったオブジェクトを指すポインタや参照を返却しようとするのは
あまりよろしくありません。
表記上の問題にやメモリ管理問題に繋がります。
つまり、オブジェクトは値として返すべきです。
ここでムーブ演算を定義して使いましょう。

ムーブとはなんぞや?

という人には更に詳しく書いてあるこちらの方々のブログを
オススメさせて頂きます。

C++のmove semantics完全に理解した - すてにゃんのガチ勢日記
本当は怖くないムーブセマンティクス - yohhoyの日記(別館)

一読すればムーブについては大体わかると思います。
すいません、短くて。


3.テンプレートを分割してコンパイル

テンプレートを利用するコードの構成方法として
合理的でわかりやすい方法が以下のように三つあります。

  1. 翻訳単位内でテンプレートを利用する前に、その定義をincludeする。
  2. 翻訳単位内でテンプレートを利用する前に、その宣言のみをincludeし、テンプレート定義はその翻訳単位内でその後includeする(利用箇所の後でもOK)
  3. 翻訳単位内でテンプレートを利用する前に、その宣言(のみ)をincludeし、他の翻訳単位でテンプレートを定義する。

ただ、技術的及び歴史的な理由から、テンプレート定義と
利用箇所を分割コンパイルする3は提供されていません。
これまで広く用いられている方法はテンプレートを利用する
全ての翻訳単位でそのテンプレートの定義をincludeして、
重複するコードの削除について、処理系によるコンパイル時の
最適化に委ねるというものです。

たとえばout.hでテンプレートout()を定義したとします。

#include<iostream>

template<typename T>
void out(const T& t)
{
    std::cerr << t;
}

そして、out()を必要とする箇所でこのヘッダをincludeします。

//ファイルout.h
#include "out.h"
// out()を利用

つまり、out()の定義とout()が依存する全ての宣言を
それぞれのコンパイル単位でincludeします。
必要に応じてコードを生成し、冗長な定義の読み取り処理を
最適化するのはコンパイラに任せます。
この方法はテンプレート関数をインライン関数と同様に処理するものです。

この方式の問題点は、out()の定義だけを目的として
includeしたはずなのに、includeされる別の宣言に誤って依存してしまうことです。
この危険性は先程の2の方法、名前空間を用いる方法、
マクロを避ける方法等で解消されます。
一般的なのはincludeする情報量を削減する方法です。
テンプレート定義の環境依存症を最小限に抑えるのが理想的と言われています。

2の方法では宣言と実装を二つに分けてユーザに両方をincludeさせます。

#include "out.h"
//out()を利用
#include "out.cpp"

これならテンプレートの実装がユーザコードに
望ましくない効果を与えてしまう機会が最小限に抑えられます。
しかし残念なことに、ユーザコードのマクロ等が
テンプレート定義に望ましくない効果を与えてしまう機会が増えてしまいます。
大規模プログラムではマクロの利用によるエラーは
見つけ出すのが非常に困難です。
テンプレートの文脈依存症は最小限に抑えるように気をつけつつ、
マクロに対しては疑ってかかるべきです。

具現化の文脈をもっと細かく制御する場合は
明示的具現化や、extern templateを利用しましょう。

以上、3つのテクニックでした。
もう一つ書こうと思ったんですが、
準備不足と資料不足で難しくなり書けませんでした。
他にも色々なテクニックがありますが、
それらは自分のブログを構築した際に
つらつら並べていこうと思います。

というわけでここまで読んでいただき有難うございました。
次はI (@wx257osn2) | Twitterさんです。