こんにちは、高木です。
タイトル通り、今回は生のポインタを使わずにC++のプログラムを書けるのか? という問題に挑戦したいと思います。
前提
ここで前提条件を固めておきたいと思います。生のポインタを本当一切使わないのであれば、まともなプログラムを書くのは無理だと思います。なぜなら、配列の要素にアクセスするにも、関数を呼び出すにも、生のポインタ無しにはできないからです。
ご存じのように、配列の要素にアクセスするには、配列型をいったん生のポインタ型に型変換した上で添数を加算する必要があります。
0 1 2 3 4 5 6 |
int value = array[3]; ↓ int value = *((int*)array + 3); |
また、関数呼出し演算子もオペランドに関数へのポインタ型を要求しますので、関数型をいったん生の関数へのポインタ型に型変換した上で呼び出すことになります。
0 1 2 3 4 5 6 |
func(); ↓ ((void(*)())func)(); |
こういうのまで避けることは無理ですので、今回目指すのは、あくまでも表面的にポインタを見えなくすることができるかということに徹したいと思います。
ポインタの用途
次にポインタの用途についておさらいしてみましょう。ポインタには、大きく分けて、次の3つの用途があると私は考えます。
- 間接参照
- 反復子
- オブジェクトの所有
現実的にはもう少しポインタを使いたくなる状況があって、次の2つも考慮した方がいいかもしれません。
- オブジェクトの生存期間のコントロール
- フリーストアの利用
これら5つを生のポインタ以外で実現することできれば、今回のテーマは達成できるのではないでしょうか? 以下、順に見ていきたいと思います。
間接参照
間接参照がしたいだけであれば、C++の参照を使えば実現できますね。ただし、C++の参照では、あとから参照先を変えることができません。この問題を解決するのはstd::reference_wrapperを使えば何とかなりそうです。
0 1 2 3 4 5 6 7 8 9 |
int a = 123; int b = 456; auto r = std::ref(a); // std::reference_wrapperを返す。 std::cout << r.get() << std::endl; r = std::ref(b); // std::reference_wrapperを返す。 std::cout << r.get() << std::endl; |
getを使わないといけないのがちょっと面倒ですが、まあ許容範囲でしょう。
反復子
配列の反復子には生のポインタを使うことになるのですが、生の代わりにstd::arrayを使うようにすれば、この問題は回避できるのではないでしょうか? また、文字列リテラルも生の配列ですが、いったんstd::string_viewなどを介して扱うようにすれば、やはり生のポインタは使わずに済みます。
あるいは、生の配列を扱う際にも、std::beginとstd::end、あるいはstd::cbeginとstd::cendを使って、結果をautoで受けるようにすれば生のポインタは見えなくなります。
0 1 2 3 4 |
int array[] { 0, 1, 2, 3 }; auto first = std::begin(array); // &array[0]を返す。 auto last = std::end(array); // &array[3]を返す。 |
文字列リテラルの場合は、ユーザー定義リテラルを使うのもひとつの手です。
0 1 2 3 4 |
using namespace std::literals; auto s = "hello"s; // std::string auto sv = "world"sv; // std::string_view |
前述したように、std::stringやstd::string_viewとして扱うのであれば、生のポインタを直接触る必要はなくなります。
オブジェクトの所有
オブジェクトを所有する上で一番手っ取り早いのは、ポインタなんか使わずにそのまま値を保持する方法です。自動オブジェクトや静的オブジェクトはもちろん、データメンバや配列の要素であっても、ポインタを使わずに値で保持すればいいのです。
生のポインタが必要になってくるのは、動的記憶域期間のオブジェクトの所有権を持とうとする場合です。よって、5.のフリーストアの利用と本質的には同じです。
これを生のポインタを使わずに実現するには、std::make_sharedまたはstd::make_uniqueを使って、std::shared_ptrやstd::unique_ptrとして扱えばいいのです。std::shared_ptrを使うのであれば、間接参照にはstd::weak_ptrを使うのがいいでしょうね。
0 1 2 3 4 5 6 7 8 |
struct A { int a; int b; }; auto sp = std::make_shared<A>(1, 2); // std::shared_ptrを返す。 auto up = std::make_unique<A>(1, 2); // std::unique_ptrを返す。 |
オブジェクトの所有は次に紹介するstd::optionalを使って実現することもできます。
オブジェクトの生存期間のコントロール
自動記憶域期間を持つオブジェクトはブロックの終端まで、一時オブジェクトであれば完結式の最後までが生存期間です。静的記憶域期間を持つオブジェクトであればプログラムの終了時まで生存期間があります。
けれども、任意のタイミングでオブジェクトを構築し、任意のタイミングでオブジェクトを解体したいことがよくあります。そういうときには普通、動的記憶域期間のオブジェクトとして扱いますので、new式で構築し、delete式で解体することになります。newやdeleteを直接使いたくなければ、前述のstd::make_sharedやstd::make_uniqueを使えばいいでしょう。
フリーストアを使ってメモリブロックの割付けや解放を行うのはそれなりにコストがかかりますので、単に生存期間をコントロールするだけの目的にスマートポインタを使うのはやり過ぎかもしれません。そういう場合にはstd::optionalがあります。他のプログラミング言語ではnullableという扱いになっていることも多いのではないと思います。
0 1 2 3 4 5 6 7 8 9 10 |
std::optional<int> o; std::cout << o.has_value() << std::endl; // 0 o = 123; std::cout << o.has_value() << std::endl; // 1 std::cout << *o << std::endl; // 123 o.reset(); std::cout << o.has_value() << std::endl; // 0 |
フリーストアの利用
前述したように、フリーストアの利用は3.オブジェクトの所有で紹介したstd::shared_ptrやstd::unique_ptrを使えば実現できます。
ざっと駆け足で見てきましたが、今回紹介したような方法を使えば、生のポインタを見かけ上排除することはできそうです。これで、ポインタが苦手だという方でも気軽にC++を使えるようになるかもしれませんね。生のポインタを使うより簡単になったかどうかは、人によって解釈が分かれるところだと思います。