🕵️

ダブルポインタならNULLチェックもダブル・・・だ?!

に公開1

数日前にダブルポインタ(ダブル型のポインタではなく、ポインタのポインタ)はダブルNULLチェックしないといけないのかって聞かれました。考えてみると確かにダブルポインタなので、以下のようにアクセスする前にどちらもNULLチェックしないといけないです。

void do_something(int** data)
{
    if (data && *data)
    {
        // data か *data か **data を使う
    }
}

考えてみれば、凄く当たり前なことですが、あんまり考えたことがなかったです。ダブルポインタはたまに見かけますが、思い返してみると、ダブルNULLチェックしているのはあんまり見たことがないです。その理由について話す前には、そもそもダブルポインタっていつ使うのでしょうか?

文字列に「char*」型を使っている場合は、文字列の配列を渡したい時は、「char**」を使ったりしますが、これはどちらかというとC言語です。
C++ の場合は、「std::list<std::string>」などを参照型で渡すと思います。
他にたまに見かけるのは、以下のようなユースケースです。

#include <iostream>
#include <string>

class Person
{
public:
    Person(std::string n)
    : m_name (n)
    {}
    std::string name() { return m_name; };

private:
    std::string m_name;
};

void alloc1(Person* p) {
    p = new Person ("alloc1");
}

void alloc2(Person** pp) {
    *pp = new Person ("alloc2");
}

int main(){
    Person* p = nullptr;
    alloc1(p);
    //std::cout << p->name() << std::endl; // クラッシュする
    alloc2(&p);
    std::cout << p->name() << std::endl;
    delete(p);

    return 0;
}

alloc1alloc2は引数のポインタ自体の値を変えますが、alloc1にはポインタを値渡しで渡しているので、ポインタの値を変えても、元々のポインタはNULLのままで、alloc1の中でアロケートしたメモリはリークします。
ポインタ自体の値を変えたい場合は、alloc2のようにダブルポインタを使わないといけないです。因みにですが、以下のように参照型でも渡せるのですが、殆ど見たことがないです。

void alloc3(Person*& p) {
    p = new Person ("alloc3");
}

でもこの問題に走るのはそもそも生ポインタを渡しているからであって、こういう場合は生ポインタを使わない方がいいです。
このように関数の中でnewでアロケートするのは、どこでdeleteされるのか、全ルートでメモリが解放されるのかが分かりづらいため、お勧めできないです。
↑のユースケースは以下のように書いた方がいいです。

void alloc(std::unique_ptr<Person>& p) {
    p = std::make_unique<Person> ("alloc");
}

int main() {
    std::unique_ptr<Person> p;
    alloc(p);
    std::cout << p->name() << std::endl;

    return 0;
}

そもそも既存の変数を渡さなくてもいい場合は、以下のようにも書けます。

std::unique_ptr<Person> alloc() {
    return std::make_unique<Person>("alloc");
}

std::unique_ptrのようなスマートポインタを使うと、メモリリークのリスクが極端に減ります。

生ポインタさえ使うのは既にお勧めできないのに、ダブルポインタなんてその2倍ぐらい厄介です。使わないといけないケースも存在すると思いますが、できるだけ使わない方がいいです。


|cpp記事一覧へのリンク|

Discussion

dameyodamedamedameyodamedame

C++はそれなりに低レベルな処理をするため、Cの呼び出し規約に従う関数を使うことも多いと思います。
なので、ポインタのポインタを使うことも多いでしょう。必要がないような書き方をしてしまうと誤解を招きます。例えば

// for C++14
#include <iostream>
#include <cstdint>
int main(int argc, char* argv[], char* envp[]) {
    using namespace std;
    cout << "argc: " << argc << endl;
    auto print_array_null_end = [](auto name, auto arr, size_t size = SIZE_MAX) {
        if (arr == NULL) {
            cout << name << ": null" << endl;
        } else {
            for (size_t i = 0; i < size && arr[i] != NULL; ++i) {
                cout << name << "[" << i << "]: " << arr[i] << endl;
            }
        }
    };
    print_array_null_end("argv", argv, argc);
    print_array_null_end("envp", envp);
    // テスト用
    // static bool calling = false;
    // if (! calling) {
    //     calling = true;
    //     static char* strs[] = {NULL};
    //     main(0, NULL, strs); // ISO的にはNG
    // }
    return 0;
}

main関数(処理系拡張含む)すらこうも書けるわけです。envpは処理系拡張ではありますが、gcc/clang/vc++と主要な処理系で対応しています。

自分で書く分にはプロジェクトの決め事に従った範囲で好きに書けますが、他人が書いたコードをメンテナンスする際にはそもそも使われてたりするので、必要です。

なお、

そもそも既存の変数を渡さなくてもいい場合は、以下のようにも書けます。

std::unique_ptr<Person> alloc() {
    return std::make_unique<Person>("alloc");
}

std::unique_ptrのようなスマートポインタを使うと、メモリリークのリスクが極端に減ります。

とありますが、自分で決めれるのであれば「既存の変数を渡さないといけない場合」というのは存在しないので、常にこう書けますし、書くべきです。またallocX()はいずれもthisに関係しないので、staticなメンバ関数にすべきです。

static std::unique_ptr<Person> alloc() {
    return std::make_unique<Person>("alloc");
}

(訂正)
メンバ関数なのかと思ったらグローバル関数だったんですね。クラスに関係しないのであればstaticは不要です。