突然だが私はコンテキストが好きだ。
先ず言葉の響きがいい。そして謎めいているからだ。
コンテキストって何だ?という疑問からこの記事を書くことにしたが
筆者の考えや経験に基づいた内容が多く、正確ではないのであまり真に受けないで頂きたい。
コンテキストの効果
コンテキスト(Context)とは、文脈を表す単語である。
例えばここに
パンを食べた
という文がある。
コンテキストは○を○した
である。
この文からはパン
と食べた
という2つの情報を得ることができる。
では次に
私は朝食にパンを食べた
コンテキストは○は○に○を○した
である。
後者の方がより詳細な情報と認識できるのはコンテキストの影響である。
このようにコンテキストを変えることで文章の情報量や意味を変えることができる。
また、後者のコンテキストが前者のそれを内包しているように、コンテキストはコンテキストを内包する。この記事もそうだが、大きな1つの文章はいくつものコンテキストによって形成されている。誤解を恐れずに書けば、最小のコンテキストは5W1H
の一部であり、最大のコンテキストは物語そのものである。
さて、前置きが長くなってしまったが
プログラミングは言語によって処理の流れを文として記述していく。
したがってプログラムの中にも多くのコンテキストが含まれているのだが
意識して読むと書き手によって様々なコンテキストが形成されていて大変興味深いものである。
プログラミングにおけるコンテキスト
筆者がプログラミングにおいて初めてコンテキスト(Context)というワードを目にしたのは
Windowsのスケルトンプログラムのサンプルコードである。
スケルトンプログラムとは、エントリポイント(WinMain)とウインドウプロシージャだけからなる非常に質素なプログラムのことである。
ウインドウに何かを描出したい時には、ウインドウプロシージャでWM_PAINT
メッセージを受け取り、円や四角形などの図形を出力することができる。描出を開始する前にBeginPaintを呼び出し、描出を終了した後にEndPaintを呼び出すというルールが定められている。
このBeginPaint
の戻り値の型がHDC
、即ちディスプレイのデバイスコンテキスト(のハンドル)である。
例えばウインドウに図形を描出したいときは以下のように書く。
if (auto hdc = BeginPaint(...)) {
Ellipse(hdc, ...); // 楕円を描出する
Rectangle(hdc, ...); // 次に矩形を描出する
EndPaint(...);
}
このように、デバイスコンテキストに描出したい図形の形状や順番を保存していくことで
流れが形成されていくのである。
しかし、プログラミングにおけるコンテキストは流れを形成するだけではなく、流れを制限する役割もある。
例えば、新しく星型の図形を描出する機能を作りたいとする。
以下のような関数を定義して直線を駆使することで実現したとする。
void Star(...) {
Line(...);
Line(...);
... // 以下省略
}
さて、Star
関数はBeginPaint
とEndPaint
の間で呼ばれなければならないが
そのような制限を関数に付与するにはどうすれば良いだろうか?
実はhdc
が存在しているということがBeginPaint
とEndPaint
の間であるということを暗に保証しているのである。
つまりhdc
を引数に受け取るようにするだけで呼び出しを制限できるというわけである。
void Star(HDC& hdc, ...) {
Line(hdc, ...);
Line(hdc, ...);
... // 以下省略
}
これこそがプログラミングにおけるコンテキストの役割である。
まさにSNSなどで稀に目にする「この流れ(コンテキスト)なら言える」というやつである。
コンテキストの使用例
前述の通りコンテキストには、文脈の構成要素を定義することと、処理の流れを制限する機能がある。
私が最もよく利用するのは単に文脈のパラメータとして使うケースである。
struct DrawParams
{
int pos_x; // 座標
int pos_y;
int color; // 色
};
このような構造体は図形を描出する流れでは頻繁に見かけるだろう。
これは単なる構造体に見えるが
上記の構造体を使うコンテキストでは座標と色が構成要素であることがわかる。
もう一つは処理の識別子として使うケースである。
例えば以下のようなマルチスレッドなプログラムで効果を発揮する。
class ContextA
{
ContextA() = default; // privateなコンストラクタ
public:
static void thread_entry() {
ContextA ctx;
// ここはスレッドaのコンテキスト
}
};
class ContextB
{
ContextB() = default;
public:
static void thread_entry() {
ContextB ctx;
// ここはスレッドbのコンテキスト
}
};
void main() {
// ここはメインのコンテキスト
std::thread a{ContextA::thread_entry}; // スレッドa
std::thread b{ContextB::thread_entry}; // スレッドb
}
この時ContextA
の参照を関数の引数に取ることで呼び出し元をスレッドaだけに制限する。
void call_from_a(ContextA&) {
// この関数はスレッドaから呼ばれている
}
もちろんContextA
にパラメータを持たせることもできるし、複数のコンテキストで関数をオーバーロードしたりもできる。
以上