D言語入門記事 ~ConstとImmutable(2)と、時々scope(1)~

アバター
tk-xleader
記事: 158
登録日時: 14年前
連絡を取る:

D言語入門記事 ~ConstとImmutable(2)と、時々scope(1)~

投稿記事 by tk-xleader » 13年前

タイトルは、リリー・フランキーのこの小説のパロディです。それにしても古いネタだ…

[1] 実引数の定値性を戻り値に反映する

 基本的に、参照型(配列、ポインタ型 クラス型)の引数は、参照先のデータを関数内部で書き換えることがないのであれば、const属性をつけておくのが一番優れた選択です。mutable/immutableのどちらも、constには暗黙に変換することが出来るからです。しかし、場合によっては、実引数の定値属性を戻り値に反映したいときがあると思います。戻り値に定値属性を反映させるというのは、実引数のmutable/const/immutableが、戻り値と一致するようにするということですが、その好例が、C言語の標準関数の、memchr関数です。
 memchr関数は、バッファの中からバイト数値を検索する関数で、対象バッファへのポインタとその長さ、検索する数値の3引数を受け取って、最初に検索対象の数値が出現したアドレスを返す関数です。見つからない場合はnullを返す関数です。今回は、これをD言語で実装してみようと思います。ただし、戻り値の定数に関する属性は、実引数と一致するようにするものだとしましょう。(本来のmemchrの仕様に反しますが、バイト数値は、ubyte型で受け取るものとして実装します。)

 まず、mutable/const/immutableの有無でオーバーロードできますから、それを利用して、戻り値に反映させるコードを書いてみます。

CODE:

void* memchr(void* buffer, ubyte value, size_t length){
	
	ubyte* begin = cast(ubyte*)buffer;
	ubyte* end = begin + length;
	
	for(; begin < end; begin++){
		if(*begin == value) return begin;
	}
	return null; //最後まで実行されたということは、見つからなかったということである。
}

const(void)* memchr(const(void)* buffer, ubyte value, size_t length){
	
	auto begin = cast(const(ubyte)*)buffer;
	auto end = begin + length;
	
	for(; begin < end; begin++){
		if(*begin == value) return begin;
	}
	return null; //最後まで実行されたということは、見つからなかったということである。
}

immutable(void)* memchr(immutable(void)* buffer, ubyte value, size_t length){
	
	auto begin = cast(immutable(ubyte)*)buffer;
	auto end = begin + length;
	
	for(; begin < end; begin++){
		if(*begin == value) return begin;
	}
	return null; //最後まで実行されたということは、見つからなかったということである。
}
 これで解決はしてますが、コピペ臭ぷんぷんのコードですね。実際、コピペして作ったコードなんですが…
 そこで、どれか1つに実装を集約して、残りの2つはそのコードへの表面的なインターフェイスとして実装することを考えます。コードの性格上、bufferの指す先を書き換えるわけではありませんが、mutableな実引数が与えられる場合もありますから、constのコードを実際の実装として、mutable/immutableの場合のコードは、そこへのサンクションコードとして実装します。

CODE:

void* memchr(void* buffer, ubyte value, size_t length){
	//実際の実装はconstバージョンで行う。
	return cast(void*) memchr(cast(const)buffer,value,length);
}

const(void)* memchr(const(void)* buffer, ubyte value, size_t length){
	
	auto begin = cast(const(ubyte)*)buffer;
	auto end = begin + length;
	
	for(; begin < end; begin++){
		if(*begin == value) return begin;
	}
	return null; //最後まで実行されたということは、見つからなかったということである。
}

immutable(void)* memchr(immutable(void)* buffer, ubyte value, size_t length){
	//実際の実装はconstバージョンで行う
	return cast(immutable) memchr(cast(const)buffer,value,length);
}
 コードの重複を削除することは出来ましたけれども、強制型変換に頼っているので、このようなコードには、潜在的にバグを埋め込む可能性があります。そこで、D言語には、inoutという属性が存在します。これを使えば、上のコードは、次のように改善することが出来ます。

CODE:

inout(void)* memchr(inout(void)* buffer, ubyte value, size_t length){
	
	auto begin = cast(inout(ubyte)*)buffer;
	auto end = begin + length;
	
	for(; begin < end; begin++){
		if(*begin == value) return begin;
	}
	return null; //最後まで実行されたということは、見つからなかったということである。
}
 つまり、inoutというのは、実引数がmutableのときはmutable、constのときはconst、そして、immutableのときはimmutableになる属性なのです。仮引数や、thisに対する属性として使うことが出来ますが、それらに対してこの属性を使った場合、戻り値の型もinout属性でなければならないという仕様があります。もちろん、書き型はconstやimmutableといった属性を使うときと全く一緒です。

[2] D言語でRAIIを実現する

 C++では、クラス型のオブジェクトが自動変数である場合などには、変数の寿命が終了するときに、確実にデストラクタが呼ばれます。これを利用して、外部リソースの確保と解放を、オブジェクトの初期化と寿命終了と一致させるというプログラミング手法があります。このことを、RAII(Resource Acquisition Is Initialization)といいます。
 しかしながら、D言語では、オブジェクトの初期化のときこそ確実にコンストラクタが呼ばれますが、その寿命が終了するタイミングが予測不能なので、通常の場合では、外部リソースの解放を明示的に実行するか、あるいはdelete演算子によって、オブジェクトの終了自体を明示的に行う必要があります。とはいうものの、後始末を明示的に実行しなければならないというのは、それを忘れる原因の一つです。

 というわけで、D言語でもRAIIをサポートしています。そのためのキーワードが、「scope」です。クラス型の変数にscope属性をつけると、スコープを脱するときに、その変数に対してdeleteが実行されます。以下、テストコード

CODE:

import std.stdio;

class Sample{
	string name;
	this(string name){
		this.name = name;
		writeln("Called Constructor of " ~ name);
	}
	~this(){
		writeln("Called Destructor of " ~ name);
	}
}

void main(){
	Sample obj1 = new Sample("object 1");
	scope Sample obj2 = new Sample("object 2");
	//*1
	
	{
		writeln("\n---------------Into Deep Scope-----------------------\n");
		scope Sample obj3 = new Sample("object 3");
		Sample obj4 = new Sample("object 4");
		scope Sample obj5 = new Sample("object 5");
		//*2
		writeln("\n---------------Return Out of Deep Scope---------------------\n");
	} //*3
	writeln("\n---------------Returned Scope-----------------------------\n");
	
	scope Sample obj6 = new Sample("object 6");
	//*4
	writeln("\n=======================End of Main=====================\n");
}

//*5
実行結果
Called Constructor of object 1
Called Constructor of object 2


---------------Into Deep Scope-----------------------

Called Constructor of object 3
Called Constructor of object 4
Called Constructor of object 5


---------------Return Out of Deep Scope---------------------

Called Destructor of object 5
Called Destructor of object 3


---------------Returned Scope-----------------------------

Called Constructor of object 6

=======================End of Main=====================

Called Destructor of object 6
Called Destructor of object 2
Called Destructor of object 4
Called Destructor of object 1


 実行結果は、コンストラクタとデストラクタの呼び出しの部分を色分けしてみました。
 見ると分かりますが、scope属性が付与されているオブジェクトは2,3,5,6の4つなんですが、それぞれ、スコープを抜けるときに、オブジェクトを生成した順番とは逆順に呼ばれていることが分かると思います。対照的に、scopeではないオブジェクトは、プログラムの最後にまとめてGCが解放していることも分かると思います。全ての変数のscope属性を取っ払うと、こうなります。

CODE:

import std.stdio;
 
class Sample{
        string name;
        this(string name){
                this.name = name;
                writeln("Called Constructor of " ~ name);
        }
        ~this(){
                writeln("Called Destructor of " ~ name);
        }
}
 
void main(){
        Sample obj1 = new Sample("object 1");
        Sample obj2 = new Sample("object 2");
        //*1
        
        {
                writeln("\n---------------Into Deep Scope-----------------------\n");
                Sample obj3 = new Sample("object 3");
                Sample obj4 = new Sample("object 4");
                Sample obj5 = new Sample("object 5");
                //*2
                writeln("\n---------------Return Out of Deep Scope---------------------\n");
        } //*3
        writeln("\n---------------Returned Scope-----------------------------\n");
        
        Sample obj6 = new Sample("object 6");
        //*4
        writeln("\n=======================End of Main=====================\n");
}
 
//*5
実行結果
Called Constructor of object 1
Called Constructor of object 2

---------------Into Deep Scope-----------------------

Called Constructor of object 3
Called Constructor of object 4
Called Constructor of object 5

---------------Return Out of Deep Scope---------------------


---------------Returned Scope-----------------------------

Called Constructor of object 6

=======================End of Main=====================

Called Destructor of object 6
Called Destructor of object 5
Called Destructor of object 4
Called Destructor of object 3
Called Destructor of object 2
Called Destructor of object 1


 全てのオブジェクトが、プログラムの終了時にまとめてGCによって破棄されているという結果になりました。GC自体は、必ずしもプログラムの最後に作動するわけではありませんが、この程度の小規模なサンプルプログラムの場合、途中でGCを発動させなければならないほど深刻なメモリ不足に陥ることがまずありませんから、プログラムの途中でGCが起動せず、最後にまとめて回収ということになっています。
 クラスにもscope属性を付与することが出来ます。その場合、そのオブジェクトはscope属性が付与された変数以外では参照できないようになります。これによって、使用者の側にRAIIを強制することが出来るのです。

前の記事「コンテキスト参照とthis (1)」 ←→ 次の記事「………To Be Determined.(未定)」

コメントはまだありません。