C++
C++ における関数スタイルのプログラミング
C++ は、実行時のコストを非常に低く (通常は 0) 抑える高度な抽象化を実現する、マルチパラダイムのシステム レベル言語です。一般に、手続き型、オブジェクト指向、汎用プログラミングなどが、 C++ に関連付けられるパラダイムです。C++ では、高度なプログラミング向けに優れたツールが用意されているため、関数スタイルのプログラミングであってもきわめて妥当性があります。
関数スタイルのプログラミングと言っても、プログラミングが厳密に関数型というわけではなく、C++ の関数型ビルディング ブロックの多くを簡単に使用できるということにすぎません。今回は、関数型プログラミングで最も重要な構造の 1 つとして、ID ではなく値を操作するという構造に重点を置いて説明します。まず、C++ に常備されている、値を操作するための強力なサポートについて説明してから、新しい C++ 11 標準ではこのサポートがラムダによってどのように拡張されるかを示します。最後に、関数型言語で長年重宝されている保護を提供しながら、C++ の特徴でもある速度を維持する不変データ構造の操作方法を紹介します。
値と ID の違い
まず、ID ではなく値を操作する意味について説明しましょう。1、2、3 といった単純な値は、簡単に識別できます。この場合の 1、2、3 は整数の定数値とも呼びます。ただし、この表現は冗長で、実際にはすべての値が定数で、値自体が変化することはありません (1 は常に 1 で、2 になることはありません)。一方、ID に関連付けられる値は、変化することがあります (x が現在 1 であっても、将来 2 に変わる可能性があります)。
残念ながら、値と値型は混同されがちです。値型には、参照ではなく値が渡されます。今回は、値の使用や値のコピーに関連するメカニズムではなく、値に注目します。ただし、値と ID の考え方の違いを見失わないように、値型がこの 2 つの考え方にどのように関係するかを確認しておくと便利です。
図 1 のコードは、値型を単純に使用する方法を示しています。
図 1 値型の使用
void Foo()
{
for (int x = 0; x < 10; ++x)
{
// Call Bar, passing the value of x and not the identity
Bar(x);
}
}
void Bar(int y)
{
// Display the value of y
cout << y << " ";
// Change the value that the identity y refers to
// Note: This will not affect the value that the variable x refers to
y = y + 1;
}
// Outputs:
// 0 1 2 3 4 5 6 7 8 9
わずかに変更を加えるだけで、変数 y が参照型になります。このとき、x と y の関係は大きく変化します (図 2 参照)。
図 2 参照型の使用
void Foo()
{
for (int x = 0; x < 10; ++x)
{
// Call Bar, passing the identity of x
Bar(x);
}
}
void Bar(int& y)
{
// Display the value of y
cout << y << " ";
// Change the value that the identity y refers to
// Note: This will affect the variable x
y = y + 1;
}
// Outputs:
// 0 2 4 6 8
図 3 に示すように、C++ の const 修飾子を使用すると、プログラマが変数に変更を加えることができなくなります。これにより、値という考え方を保持し続けることができます (ただし、C++ でほとんどの場合がそうであるように、この保護を破る方法も少なくとも 1 つは存在します。詳細については、"const correct" でない以前のコードで使用することを目的とする const_cast を参照してください)。
図 3 const 修飾子
void Foo()
{
for (int x = 0; x < 10; ++x)
{
// Call Bar, passing the identity of x,
// yet the value of x will be protected by the const
Bar(x);
}
}
void Bar(const int& y)
{
// Use the value of y
cout << y << " ";
// Attempt to change the value of what the identity y refers to
y = y + 1; // This line will not compile because y is const!
}
図 3 では、y が参照によって渡されていますが、y の値は const 修飾子によってコンパイル時に保護されることに注意してください。これにより、C++ プログラマは、ID ではなく値を操作しながら、大きなオブジェクトを渡すことができる効率の良いメソッドを手に入れることになります。
この const 修飾子により、ほとんどの関数型プログラミング言語と同様の不変データ型が C++ にもたらされます。ただし、このような不変データ型の処理は難しくなります。また、小さな変更を加えるたびに、大きなオブジェクトのディープ コピー (完全なコピー) を作成するのは効率的ではありません。にもかかわらず、標準の C++ に、値を操作するという考え方が (非常に純粋な考え方でなくても) 常に存在することは明らかです。
値型のサポートは、コピー コンストラクターと代入演算子によって、ユーザー定義型に拡張されます。C++ のコピー コンストラクターと代入演算子を使用すると、ユーザー定義型によってオブジェクトのディープ コピーを作成できます。C++ のコピー コンストラクターは、シャロー コピーを作成するためにも実装できますが、値のセマンティクスを確保する必要があることを忘れないでください。
C++ 11 における関数スタイル プログラミングのサポート
C++ 11 では、関数スタイルのプログラミングに数多くの新しいツールが導入されています。おそらく最も重要なのは、C++ でラムダ式 (クロージャまたは匿名関数とも呼ばれます) がサポートされるようになったことです。ラムダ式を使うと、以前は実用的にはなり得なかった方法でコードを構成できるようになります。これまで、この機能はファンクタを使って利用できましたが、強力ではあっても実用的ではありませんでした (実際、C++ のラムダ式は背後で匿名ファンクタを作成します)。ラムダ式によってコードがどのように改善されるか、C++ の標準ライブラリ (STL) を使用する単純な例を使って示します (図 4 参照)。
図 4 ラムダ式の使用
void Foo()
{
vector<int> v;
v.push_back(1);
v.push_back(2);
v.push_back(3);
for_each(begin(v), end(v), [](int i) {
cout << i << " ";
});
}
// Outputs:
// 1 2 3
この例では、for_each 関数で vector の各要素にラムダ式を適用しています。C++ のラムダ式は、可能な限りインラインで使用するよう設計されています。したがって、ラムダ式は手動でコードを作成するのと同程度高速に実行できます。
C++ は、現在ラムダ式を有する数多くの命令型言語の 1 つにすぎませんが、(関数型プログラミング言語と同様に) ID ではなく値を使用するという確保できるという点が、C++ のラムダ式を特別なものにしています。関数型プログラミング言語では、変数を不変にすることでこれを実現しますが、C++ では、取得時の制御を可能にすることでこれを実現します。図 5 のコードについて考えてみましょう。
図 5 参照による取得
void Foo()
{
int a[3] = { 11, 12, 13 };
vector<function<void(void)>> vf;
// Store lambdas to print each element of an array
int ctr;
for (ctr = 0; ctr < 3; ++ctr) {
vf.push_back([&]() {
cout << "a[" << ctr << "] = " << a[ctr] << endl;
});
}
// Run each lambda
for_each(begin(vf), end(vf), [](function<void(void)> f) {
f();
});
}
// Outputs:
// a[3] = -858993460
// a[3] = -858993460
// a[3] = -858993460
このコードでは、取得をすべて参照によって行っています。他言語のラムダ式では、これが標準の動作です。しかし、取得する変数が不変でなければ、参照による取得は複雑な作業になります。ラムダ式を使用した経験がない方は、コードの出力をおそらく次のように予測することでしょう。
a[0] = 11
a[1] = 12
a[2] = 13
しかし、実際の出力は異なり、プログラムがクラッシュすることもあります。それは、変数 ctr を参照によって取得しているため、すべてのラムダ式で ctr の最終値 (つまり、for ループの終了値の 3) を使用し、境界を越えて配列にアクセスするためです。
また、ラムダ式に使用するために for ループの外まで変数 ctr を保持するには、for ループの外で ctr 変数の宣言を行うことも必要です。値型変数を適切な領域まで移動する必要をなくしている言語もありますが、これは、ラムダ式で変数 ctr の ID ではなく ctr の値を使用する必要があるという問題を本当に解決していることにはなりません (他言語においては、一時変数に明示的にコピーするという回避策があります。しかし、これでは何が行われているかが少しわかりづらくなります。また、元の変数も取得されて使用可能な状態のままであるため、エラーが発生しやすくなります)。
図 6 に示すように、C++ にはラムダ式による取得を簡単に制御できる単純な構文が用意されているため、値を使用するという考え方を保持することができます。
図 6 ラムダ式の取得を制御する C++ 構文
[] | 何も取得しない (ラムダ式の最初の例で使用したかったもの) |
[&] | すべて参照によって取得する (ラムダ式の従来の動作。ただし、関数型プログラミングによる値を重要視する動作とは異なる) |
[=] | すべて値によって取得する (値の考え方は保持されても、ラムダ式の実用性が制限される。また、大きなオブジェクトをコピーするのに高いコストがかかることがある) |
[&ctr] | ctr のみを参照によって取得する |
[ctr] | ctr のみを値によって取得する |
[&,ctr] | ctr は値によって取得し、その他すべては参照によって取得する |
[=,&v] | v は参照によって取得し、その他すべては値によって取得する |
[&, ctr1, ctr2] | ctr1 と ctr2 は値によって取得し、その他すべては参照によって取得する |
図 6 を見ると、ラムダ式で変数と値を取得する方法を、プログラマが完全に制御できることがわかります。しかし、これによって値を使用するという考え方は保持できますが、複雑なデータ構造を値として効率よく操作することには役に立ちません。
不変データ型
不足しているのは、一部の関数型プログラミング言語にあるような効率的な不変データ構造です。このような言語では、不変データ構造は共通データを共有するため、非常に大きくなった場合でも、効率性を高めることができます。データを共有するデータ構造を C++ で作成するのは簡単です。データを動的に割り当てるだけで、各データ構造がそのデータへのポインターを保持します。残念ながら、共有変数の有効期間を管理するのが難しくなります (ガベージ コレクターが普及したのはこのためです)。さいわい、C++ 11 には、std::shared_ptr テンプレート クラスで共有変数を操作するための洗練されたソリューションが用意されています (図 7 参照)。
図 7 変数の共有
void Foo()
{
// Create a shared int
// (dynamically allocates an integer
// and provides automatic reference counting)
auto sharedInt = make_shared<int>(123);
// Share the integer with a secondShare
// (increments the reference count)
shared_ptr<int> secondShare(sharedInt);
// Release the pointer to the first integer
// (decrements the reference count)
sharedInt = nullptr;
// Verify the shared int is still alive
cout << "secondShare = " << *secondShare << endl;
// Shared int is automatically de-allocated
// as secondShare falls out of scope and the reference
// count falls to zero
}
// Outputs:
// secondShare = 123
図 7 のコードに、std::shared_ptr とそのヘルパー メソッドである std::make_shared の単純な使用方法を示します。std::shared_ptr を使用すると、メモリ リークを恐れることなくデータ構造間でデータを共有しやすくなります (循環参照を使用していない場合に限ります)。std::shared_ptr では、基本的なスレッド セーフ性が保証されます。また、ロックを使用しない設計を採用しているため高速実行が可能です。ただし、std::shared_ptr で提供される基本的なスレッド セーフ性の保証は、std::shared_ptr が指すオブジェクトまで自動的に拡張されないことに注意してください。それでも、std::shared_ptr により、std::shared_ptr が指すオブジェクトのスレッド セーフ性の保証が縮小されないことは保証されます。不変オブジェクトは作成されると変化することがないため、強力なスレッドセーフ性の保証が本質的に提供されます (実際、適切なスレッド セーフ性の保証を含むような識別できる方法で変化することはありません)。そのため、不変オブジェクトと std::shared_ptr を使用すると、その組み合わせによって不変オブジェクトの強力なスレッド セーフ性の保証を保持することができます。
これで、データを共有する可能性のある単純な不変クラスを容易に作成できるようになります (図 8 参照)。
図 8 データを共有する場合の Immutable クラス
class Immutable
{
private:
// Use a normal double, copying is cheap
double d_;
// Use a shared string, because strings can be very large
std::shared_ptr<std::string const> s_;
public:
// Default constructor
Immutable()
: d_(0.0),
s_(std::make_shared<std::string const>(""))
{}
// Constructor taking a string
Immutable(const double d, const string& s)
: d_(d),
s_(std::make_shared<std::string const>(s))
{}
// Move constructor
Immutable(Immutable&& other)
: s_()
{
using std::swap;
swap(d_, other.d_);
swap(s_, other.s_);
}
// Move assignment operator
Immutable& operator=(Immutable&& other)
{
swap(d_, other.d_);
swap(s_, other.s_);
return *this;
}
// Use default copy constructor and assignment operator
// Getter Functions
double GetD() const
{
// Return a copy because double is small (8 bytes)
return d_;
}
const std::string& GetS() const
{
// Return a const reference because string can be very large
return *s_;
}
// "Setter" Functions (always return a new object)
Immutable SetD(double d) const
{
Immutable newObject(*this);
newObject.d_ = d;
return newObject;
}
Immutable SetS(const std::string& s) const
{
Immutable newObject(*this);
newObject.s_ = std::make_shared<std::string const>(s);
return newObject;
}
};
図 8 は少々長いコードですが、ほとんどの部分はコンストラクターや代入演算子の定型コードです。最後の 2 つの関数は、オブジェクトを不変にするために重要となる部分です。SetS メソッドと SetD メソッドでは、元のオブジェクトは変更しないで新しいオブジェクトを返します (SetS メソッドと SetD メソッドをメンバーとして含めると便利ですが、実際には元のオブジェクトを変更しているわけではないので少しごまかしがあります。より明確なソリューションについては、図 9 と図 10 の ImmutableVector を参照してください)。図 11 に、Immutable クラスの動作を示します。
図 9 スマートな ImmutableVector テンプレート クラスの使用
template <class ImmutableVector>
void DisplayImmutableVector(const char* name, const ImmutableVector& v)
{
using namespace std;
cout << name << ".Size() = " << v.Size()
<< ", " << name << "[] = { ";
for (size_t ctr = 0; ctr < v.Size(); ++ctr) {
cout << v[ctr] << " ";
}
cout << "}" << endl;
}
void ImmutableVectorTest1()
{
// Create an ImmutableVector with a branching size of four
ImmutableVector<int, 4> v;
// Another ImmutableVector (we will take a copy of v at element 6)
ImmutableVector<int, 4> aCopyOfV;
// Push 16 values into the vector (this will create a two level tree).
// Note that the vector is being assigned to itself. The
// move constructor insures this is not very expensive, but
// if a copy was made at any point the copy would remain
// unchanged, but continue to share the applicable data with
// the current version.
for (int ctr = 0; ctr < 10; ++ctr) {
v = AppendValue(v, ctr);
if (ctr == 6) aCopyOfV = v;
}
// Display the contents of the vectors
DisplayImmutableVector("v", v);
DisplayImmutableVector("aCopyOfV", aCopyOfV);
}
// Outputs:
// v.Size() = 10, v[] = { 0 1 2 3 4 5 6 7 8 9 }
// aCopyOfV.Size() = 7, aCopyOfV[] = { 0 1 2 3 4 5 6 }
図 10 ImmutableVector の操作に使用するメソッド
void ImmutableVectorTest2()
{
ImmutableVector<int, 4> v;
v = AppendValue(v, 1);
v = AppendValue(v, 2);
v = AppendValue(v, 3);
int oldValue = v.Back();
auto v1 = TruncateValue(v);
auto v2 = SubstituteValueAtIndex(v, 0, 3);
auto v3 = GenerateFrom(v, [](ImmutableVector<int, 4>::MutableVector& v) {
v[0] = 4;
v[1] = 5;
v[2] = 6;
v.PushBack(7);
v.PushBack(8);
});
auto v4 = GenerateFrom(v3, [](ImmutableVector<int, 4>::MutableVector& v4) {
using namespace std;
cout << "Change v4 by calling PopBack:" << endl;
cout << "x = v4.PopBack()" << endl;
int x = v4.PopBack();
cout << "x == " << x << endl;
cout << endl;
});
// Display the contents of the vectors
DisplayImmutableVector("v", v);
DisplayImmutableVector("v1", v1);
DisplayImmutableVector("v2", v2);
DisplayImmutableVector("v3", v3);
DisplayImmutableVector("v4", v4);
}
// Outputs:
// Change v4 by calling PopBack:
// x = v4.PopBack()
// x == 8
//
// Resulting ImmutableVectors:
// v.Size() = 3, v[] = { 1 2 3 }
// v1.Size() = 2, v1[] = { 1 2 }
// v2.Size() = 3, v2[] = { 3 2 1 }
// v3.Size() = 5, v3[] = { 4 5 6 7 8 }
// v4.Size() = 4, v4[] = { 4 5 6 7 }
図 11 Immutable クラスの動作
using namespace std;
void Foo()
{
// Create an immutable object
double d1 = 1.1;
string s1 = "Hello World";
Immutable a(d1, s1);
// Create a copy of the immutable object, share the data
Immutable b(a);
// Create a new immutable object
// by changing an existing immutable object
// (Note the new object is returned)
string s2 = "Hello Other";
Immutable c = a.SetS(s2);
// Display the contents of each object
cout << "a.GetD() = " << a.GetD() << ", "
<< "a.GetS() = " << a.GetS()
<< " [address = " << &(a.GetS()) << "]" << endl;
cout << "b.GetD() = " << b.GetD() << ", "
<< "b.GetS() = " << b.GetS()
<< " [address = " << &(b.GetS()) << "]" << endl;
cout << "c.GetD() = " << c.GetD() << ", "
<< "c.GetS() = " << c.GetS()
<< " [address = " << &(c.GetS()) << "]" << endl;
}
// Outputs:
// a.GetD() = 1.1, a.GetS() = Hello World [address = 008354BC]
// b.GetD() = 1.1, b.GetS() = Hello World [address = 008354BC]
// c.GetD() = 1.1, c.GetS() = Hello Other [address = 008355B4]
オブジェクト b とオブジェクト a が同じ文字列を共有していることに注意してください (どちらの文字列も同じアドレスです)。関連する getter と setter を備えた新たなフィールドを追加するのは簡単です。このコードでも良いのですが、作業効率を高めるときはコンテナーに合わせて拡張するのが少し難しくなります。たとえば、単純な ImmutableVector では、配列の各要素を示す共有ポインターの一覧を保持できる場合があります。単純な ImmutableVector が変更されると、共有ポインターの配列全体をコピーする必要があり、配列の各 shared_ptr 要素で参照カウントを増やす必要があることになり、追加のコストが発生します。
しかし、データ構造でほとんどのデータを共有し、コピーを最小限に抑えることができる手法があります。この手法では、なんらかの形式のツリーを使用して、変更によって直接影響を受けるノードのみコピーが必要になるようにします。図 12 では、単純な ImmutableVector とスマートな ImmutableVector を対比しています。
図 12 単純な ImmutableVectors とスマートな ImmutableVectors の比較
このツリーを使った手法は適切に拡張されるため、コピーが必要なツリーの割合は、要素数が増えても最小限に抑えられます。さらに、分岐している要素 (つまり、各ノードには 2 つ以上の子があります) を調整することで、メモリ オーバーヘッドとノードの再利用のバランスをとることができます。
筆者が作成したスマートな ImmutableVector テンプレート クラスは、archive.msdn.microsoft.com/mag201208CPP (英語) からダウンロードできます。図 9 は、この ImmutableVector クラスを使用する方法を示しています (既に説明したように、ImmutableVector の不変という性質がユーザーにわかりやすくなるように、ImmutableVector では新しいバージョンを生成するすべての操作に対して静的メンバー関数を使用しています)。
読み取り専用の操作では、vector を通常の vector と同様に使用できます (この例では反復子を実装していませんが、反復子の実装はきわめて簡単です)。書き込み操作では、AppendValue 静的メソッドと TruncateValue 静的メソッドが、元のオブジェクトを保持して、新しい ImmutableVector を返します。残念ながら、この処理に配列添え字演算子を使用するのは合理的ではありません。そこで、配列添え字演算子を読み取り専用にして (配列添え字演算子は const 参照を返します)、SubstituteValueAtIndex 静的メソッドを使用しました。しかし、1 ブロックのコードで、配列添え字演算子を使用して大量の変更を行えるとよいでしょう。これを行うために、ImmutableVector にはラムダ式 (またはその他のファンクタ) を利用する GenerateFrom 静的メソッドが用意されています。このラムダ式では、パラメーターとして MutableVector への参照を受け取ります。これにより、通常の std::vector のように自由に変更できる一時的な MutableVector で、ラムダ式の処理を実行することができます。図 10 の例では、ImmutableVector で操作を行うためのさまざまなメソッドを示しています。
GenerateFrom 静的メソッドの長所は、命令型の手法で GenerateFrom 静的メソッドのコードを記述しながら、安全に共有できる不変オブジェクトを作成できることです。GenerateFrom 静的メソッドは、このメソッドがラムダ式の終了直後にラムダ式に渡した MutableVector を無効にすることで、基盤となっている ImmutableVector への許可されていないアクセスを防ぐことができます。ImmutableVector では強力なスレッド セーフ性の保証が提供されますが、ImmutableVector の MutableVector ヘルパー クラスではこの保証が提供されないことにも注意してください (また、MutableVector は、他のスレッドに渡されずにローカルのラムダ式内のみで使用することを前提としています)。今回の実装では、Change メソッド内の複数の変更に対する最適化も行っているため、一時的なツリーで発生する再構築処理を最小限にして、優れたパフォーマンスの向上を実現することができます。
まとめ
今回は、C++ コードで関数型プログラミングを使用する方法について簡単に説明しました。また、ラムダ式などの C++ 11 の機能を使用すると、使用するパラダイムに関係なく関数型プログラミングを使用する感覚を味わうことができます。
David Cravey は、C++ でのプログラミングをおそらく十分すぎるほど楽しんでいる Visual C++ MVP です。彼は、地域の C++ ユーザーのグループや大学で講演を行っています。米国テキサス州フォート ワースの TEKsystems を介して、NCR で勤務しています。
この記事のレビューに協力してくれた技術スタッフの Giovanni Dicanio、Stephan T. Lavavej、Angel Hernández Matos、Alf P. Steinbach、および David Wilkinson に心より感謝いたします。