std::shared_ptr (c++11)

std::shared_ptr です。auto_ptr が deprecated(廃止予定)になりましたので、今後はこの shared_ptr, unique_ptr がスマートポインタの主流になっていくことでしょう。

 

 

 

 

1. まずは shared_ptr を使ってみる

1-1. その1

shared_ptr は自動的にメモリ解放まで行ってくれることが魅力です。
この辺りを確認しやすいサンプルを以下に提示します。

#include "stdafx.h"

#include <iostream>
#include <string>
#include <memory>
#include <vector>

using namespace std;

struct Data {
    int id;
    std::string name;

    Data()
        : Data(0, "") {}
    Data(int id, const std::string& name)
        : id(id), name(name)
    {
        std::cout << "ctor" << std::endl;
    }

    ~Data()
    {
        std::cout << "dtor" << std::endl;
    }
};


int main()
{
    {
        vector<shared_ptr<Data>> v1;

        v1.push_back(shared_ptr<Data>(new Data(1, "foo")));
        v1.push_back(shared_ptr<Data>(new Data(2, "var")));

        for (auto data : v1) {
            cout << data->id << ", " << data->name << endl;
        }
    }

    {
        vector<shared_ptr<Data>> v2;

        v2.emplace_back(new Data(3, "foo"));
        v2.emplace_back(new Data(4, "bar"));

        for (auto data : v2) {
            cout << data->id << ", " << data->name << endl;
        }
    }

    return EXIT_SUCCESS;
}

実行結果

デストラクタが自動的に実行されてメモリ解放が行われていることがわかります。

 

評価環境:

 

1-2. その2

Let's boost のホームページからサンプルプログラムを引用、少し修正して以下に掲載します。

#include <iostream>   // cout, EXIT_SUCCESS
#include <string>     // string
#include <memory>     // shared_ptr
#include <vector>     // vector

using namespace std;

int main(int argc, char* argv[])
{
    typedef shared_ptr<string> strPtr;

    auto s = strPtr(new string("pen."));
    vector< strPtr > v;

    // vectorに入れたり。
    v.push_back( strPtr(new string("this")) );
    v.push_back( strPtr(new string("is")) );
    v.push_back( strPtr(new string("a")) );
    v.push_back(s);

    cout << *s << endl;
   
    // sをpush_backで他にコピーしたからと言って使えなくなったりしない。
    // auto_ptr, unique_ptr だと使えなくなる。   

    // vector に保存した文字列を出力
    for ( strPtr pStr : v)       // ranged-for で表記
    {
        cout << *pStr << " ";
    }
    cout << endl;


    return EXIT_SUCCESS;
}
// ここで全て delete される。

出力結果:

評価環境:

 

2. make_shared

c++11 から make_shared が加わりました。shared_ptr を使う場合は極力これを使うのも良いでしょう。
ただし、shard_ptr が c++17 で 配列T に対応したことに伴って make_shared も c++20 で仕様拡張を予定しています。
逆に言えば、 c++20 までは 配列T に未対応ですので注意が必要です。

参考URL:

 

#include "stdafx.h"

#include <iostream>     // cout, endl
#include <string>       // string
#include <memory>       // shared_ptr, make_shared
#include <vector>       // vector

using namespace std;

class Data {
private:
    int id_;
    std::string name_;

public:
    Data()
        : Data(0, "") {}
    Data(int id, const std::string& name)
        : id_(id), name_(name)
    {
        std::cout << "ctor" << std::endl;
    }

    ~Data()
    {
        std::cout << "dtor" << std::endl;
    }

    int GetId()
    {
        return id_;
    }

    std::string GetName()
    {
        return name_;
    }
};


int main()
{
    vector<shared_ptr<Data>> v1;

    v1.push_back(make_shared<Data>(1, "foo"));
    v1.push_back(make_shared<Data>(2, "bar"));

    for (auto data : v1) {
        cout << data->GetId() << ", " << data->GetName() << endl;
    }

    return EXIT_SUCCESS;
}

実行結果

評価環境:

 

3. 関数の引数に使う

関数の引数に shared_ptr を渡してみます。
関数に shared_ptr を渡したり、関数の中で shared_ptr の値を変更したり、といったことができます。

#include <iostream>     // cout, EXIT_SUCCESS
#include <string>       // string
#include <memory>       // memory
#include <vector>       // vector


using namespace std;

typedef shared_ptr<string> strPtr;


void disp_vector(const vector<strPtr> v)
{
    for (strPtr s : v)
    {
        cout << *s << " ";
    }
    cout << endl;
}


// read only
void foo1( const vector<strPtr> v )
{
    disp_vector(v);
}

// change value
void foo2( vector<strPtr>& v )
{
    disp_vector(v);

    *v[3] = "book.";
    v.push_back(strPtr(new string("(insert string.)")));

    disp_vector(v);
}

int main(int argc, char* argv[])
{
    auto s = strPtr(new string("pen."));
    vector< strPtr > v;

    // vectorに入れたり。
    v.push_back(strPtr(new string("this")));
    v.push_back(strPtr(new string("is")));
    v.push_back(strPtr(new string("a")));
    v.push_back(s);

    cout << *s << endl; // sをpush_backで他にコピーしたからと言って使えなくなったりしない

    // vector に保存した文字列を出力
    disp_vector(v);

    // 関数を実行してみましょう
    cout << "[foo1]" << endl;
    foo1(v);
    cout << "[foo2]" << endl;
    foo2(v);

    // vector に保存した文字列を出力
    cout << "[after return from foo2]" << endl;
    disp_vector(v);

    return EXIT_SUCCESS;
}
// ここで全て delete される。

 

4. 関数の引数に使う(その2)

もう少し複雑な適用例を示します。
クラスと関数オーバーロードへの適用を例に示します。
smart_ptr が最後にちゃんと解放されていることがわかります。
smart_ptr と合わせて生のポインタとオート変数への適用も併記するので、使い方などを比較してみてください。

#include "targetver.h"

#include <stdio.h>
#include <tchar.h>
#include <iostream>     // cout, endl
#include <string>       // string
#include <memory>       // shared_ptr

using namespace std;


class Person{
private:
    string  name_;
    string  phone_;

public:
    Person()
        : Person("", "")                        // 委譲コンストラクタ、VS2013以降 OK、VS2012以前 NG
//      : name_(""), phone_("")                 // VS2012以前はこちらで
    {
    }

    Person(Person& rhs)
        : Person(rhs.name_, rhs.phone_)         // 委譲コンストラクタ、VS2013以降 OK、VS2012以前 NG
//      : name_(rhs.name_), phone_(rhs.phone_)  // VS2012以前はこちらで
    {
    }

    Person(string name, string phone)
        : name_(name), phone_(phone)
    {
        cout << "Person::Person(" << name_ << ")" << endl;
    }

    virtual ~Person()
    {
        cout << "Person::~Person(" << name_ << ")" << endl;
    }

    const std::string getName() const
    {
        return name_;
    }

    const std::string getPhone() const
    {
        return phone_;
    }
};


/*==================================*/
/* グローバルな演算子オーバーロード */
/*==================================*/
// shared_ptr<Person> 用
std::ostream& operator<<(std::ostream& os, const shared_ptr<Person> rhs)
{
    return (os << '(' << rhs->getName() << ',' << rhs->getPhone() << ')');
}

// Person* 用
std::ostream& operator<<(std::ostream& os, const Person* rhs)
{
    return (os << '(' << rhs->getName() << ',' << rhs->getPhone() << ')');
}

// Person 用
std::ostream& operator<<(std::ostream& os, const Person& rhs)
{
    return (os << '(' << rhs.getName() << ',' << rhs.getPhone() << ')');
}

// shared_ptr<Person> 用
void PrintPerson(shared_ptr<Person> person)
{
    cout << person << endl;
}

// Person* 用
void PrintPerson(Person* person)
{
    cout << person << endl;
}

// Person 用
void PrintPerson(Person& person)
{
    cout << person << endl;
}


int _tmain(int argc, _TCHAR* argv[])
{
    /*========*/
    /* 前処理 */
    /*========*/
    shared_ptr<Person> foo(new Person("foo", "090-****-0123"));
    shared_ptr<Person> bar(new Person("bar", "090-****-5555"));

    auto ptPerson = new Person("raw pointer person", "050-****-1234");
    Person autoPerson("auto person", "090-****-1943");

    /*========*/
    /* 処理部 */
    /*========*/
    PrintPerson(foo);
    PrintPerson(bar);
    PrintPerson(ptPerson);
    PrintPerson(autoPerson);

    /*========*/
    /* 後処理 */
    /*========*/
    delete ptPerson;             // 生のポインタは delete による解放が必要です。

    return EXIT_SUCCESS;
}

 

実行結果

foo, bar の2つの smart_ptr がちゃんと解放されていることがわかります。

評価環境:

 

5. shared_ptr で動的配列を使う

5-1. c++17 以前の環境の場合

shared_ptr はポインタが無効になった時 delete を使用します。つまり delete[]を必要とする動的配列を使用する場合は上記のような方法そのままでは使用できません。 boost::shared_array みたいなものは 標準c++ にはありません。
ただ、std::default_delete は配列に対する特殊化が提供されています。これ(std::default_delete)を使って shared_ptr で動的配列を使用する例を以下で紹介します。

 

■参考URL

 

スマートポインタの本家 boost はバージョン 1.53 以降で boost::shared_ptr が配列(T[])に対応しました。標準にもいずれフィードバックされることでしょう。(c++17 で採用されました。)
また標準ではC++11で採用された std::unique_ptr は delete, delete[] を自動判断する機構が入っています。std::unique_ptr を使用することも検討しましょう。
なお、 shared_ptr の T[] 対応は C++14 にも入りませんでした。C++17をスコープにしたTSに入ったようです。あと3年の我慢です。

#include <iostream>     // cout, EXIT_SUCCESS
#include <string>       // string
#include <memory>       // shared_ptr, default_delete
#include <vector>       // vector

using namespace std;

struct Data {
    int id;
    std::string name;

    Data()
        : Data(0, ""){}
    Data( int id, const std::string& name )
        : id(id), name(name)
    {
        std::cout << "ctor" << std::endl;
    }

    ~Data()
    {
        std::cout << "dtor" << std::endl;
    }
};

void disp( std::shared_ptr<Data> p, size_t n )
{
    for (std::size_t i = 0; i < n; ++i) {
        const Data& x = p.get()[i];
        std::cout << x.id << ", " << x.name << std::endl;
    }
}

int main()
{
    std::size_t n = 3;
    std::shared_ptr<Data> p(new Data[n], std::default_delete<Data[]>());
   
    p.get()[0] = { 3, "Alice" };
    p.get()[1] = { 1, "Bob" };
    p.get()[2] = { 4, "Carol" };

    disp(p, n);

    std::cout << "end main" << std::endl;
}

確かに配列をスマートポインタで扱えるようになりましたが、get()で生のポインタを取り出ししたのちに[]により指定する必要がある、など少々不便な感じがします。つまり shared_ptr は[]のオペレータが無いのです。boost::shared_array の方が使い勝手が良かったように感じます。c++14 で shared_array が追加されるか、shared_ptr に []のオペレータが追加されるか、などを期待します。(後日追記: c++17 で採用されました。)
配列を使う場合はコンテナ(std::vector, std::array, std::list)を使いなさい、ってのが背景にあるのかもしれません。

 

5-2. c++17 以降の環境の場合

c++17 にて shared_ptr が T[] および T[N] に対応しました。これで unique_ptr と同じように簡単に配列を実装できます。

 

#include "stdafx.h"

#include <iostream>
#include <memory>

using namespace std;

int main()
{
    // std::shared_ptr の例
    // default_delete の特殊化宣言は不要。delete[]が実行される。
    // []演算子もある。
    size_t  n = 10;
    shared_ptr<int[]> data(new int[n]);

    cout << "std::shared_ptr<int> : ";
    for (size_t i = 0; i < n; ++i) {
        data[i] = i;
    }
    for (size_t i = 0; i < n; ++i) {
        cout << data[i] << " ";
    }
    cout << endl;

    return EXIT_SUCCESS;
}

 

評価環境:

 

6. malloc, free を shared_ptr に置き換える

6-1. c++17 以前の環境の場合

c 言語で規定されている malloc, calloc でメモリを動的に取得した場合、解放に使用する関数は delete や delete[] ではなく free を使用します。デアロケータを指定することで shared_ptr のメモリ解放を free で行えるようにします。

#include <iostream>     // cout, EXIT_SUCCESS
#include <memory>       // shared_ptr
#include <cstdlib>      // malloc, free

using namespace std;

int main()
{
    const size_t    n = 256;

    shared_ptr<int> memory( static_cast<int*>(malloc(n*sizeof(int))), free);

    // いちおうメモリ取得できたかを確認のうえで使用しましょう。
    if ( memory.get() != nullptr ){
        for ( size_t i=0; i<n; ++i ){
            memory.get()[i] = (int)i;
        }
        for (size_t i = 0; i < n; ++i){
            cout << memory.get()[i] << ", ";
        }
        cout << endl;
    }

    return EXIT_SUCCESS;
    // ここで free が実装されて memory を開放
}

 

6-2. c++17 以降の環境の場合

前述の例だと "memory.get()[i]" というに配列へアクセスするのが気持ち悪い。やっぱり "memory[i]" って使いたいです。
c++17 にて shared_ptr が T[] および T[N] に対応しました。これで unique_ptr と同じように簡単に配列を実装できる、と思われます。

VS2019でテストした感じだと意図通りに "memory[i]" で使えて、ちゃんと free もやってくれてそうに見えます。
unique_ptr と使い方が違うことが不安材料です。

#include <iostream>     // cout, EXIT_SUCCESS
#include <memory>       // shared_ptr, unique_ptr
#include <cstdlib>      // malloc, free

using namespace std;

int main()
{
    const size_t    n = 256;

    std::shared_ptr<int[]> memory(static_cast<int*> (malloc(n * sizeof(int))), std::free);
    if (memory.get() == nullptr) {
        // malloc失敗
    }
    else {
        // malloc成功
        for (size_t i = 0; i < n; ++i) {
            memory[i] = (int)i;
        }
        for (size_t i = 0; i < n; ++i) {
            cout << memory[i] << ", ";
        }
        cout << endl;
    }

    return EXIT_SUCCESS;
    // ここで free が実装されて memory を開放
}

 

 

7. fopen, fclose を shared_ptr に置き換える

これは実装例をいくつか書いてみます。しっくりくるやつを使ってみてください。

シンプルにやるとこんな感じでしょうか。

#include <stdio.h>  // fopen, fclose
#include <stdlib.h>
#include <memory>   // shared_ptr

#pragma warning(disable : 4996) // fopen, fclose の使用がエラーになるため

void custom_fclose(FILE* fp){
    if (fp != nullptr){
        fclose(fp);
    }
}

int main(int argc, char* argv[])
{
    std::shared_ptr<FILE>   fp(fopen("fopen_test.txt", "w"), custom_fclose);

    if (fp.get() != nullptr){
        // fopen成功
        fprintf(fp.get(), "test\n");
    }

    return EXIT_SUCCESS;
    // ここで custom_fclose が自動実行されます
}

これを Lambda式 で記載するとこんな感じでしょうか。

#include <stdio.h>          // FILE, fopen, fclose
#include <stdlib.h>
#include <memory>           // shared_ptr

#pragma warning(disable : 4996)

int main(int argc, char* argv[])
{
    std::shared_ptr<FILE>   fp(fopen("fopen_test.txt", "w"), [&](FILE* fp){
        if (fp != nullptr){
            fclose(fp);
        }
    });

    // 一応ファイルオープンの成功/失敗を確認してからファイルを使用
    if (fp.get() != nullptr){
        // fopen成功
        fprintf(fp.get(), "test\n");
    }

    return EXIT_SUCCESS;
    // ここで custom_fclose が自動実行されます
}

ファクトリーパターン的に shared_ptr を返す関数を作成するならばこんな感じでしょうか。

#include <cstdio>       // fopen, fclose
#include <iostream>     // cout, EXIT_SUCCESS
#include <memory>       // shared_ptr

using namespace std;

#pragma warning(disable : 4996)

std::shared_ptr<std::FILE> make_file(const char * filename, const char * flags)
{
    // fclose に NULL を渡して実行してはいけない。
    // このため下記のような分岐処理が必要となる。
    std::FILE * const fp = std::fopen(filename, flags);
    return fp ? std::shared_ptr<std::FILE>(fp, std::fclose) : std::shared_ptr<std::FILE>();
}

int main()
{
    auto fp = make_file("hello.txt", "wb");

    // fp.get() をチェック
    // fopen に失敗していたら fp.get() は NULL
    if (fp.get()){
        fprintf(fp.get(), "Hello world.");
    }
}

私的には Lambda式 を使った2番目のやつが一番シンプルで好きかな。

 

 

■参考URL

 


記載: 2013年12月1日 木下英俊
           2014年3月9日 追記/修正
           2014年9月6日 ソースコードを表記を「google-code-prettify」によるものへ変更
           2018年5月4日 いろいろアップデート。c++17 に関わる make_shared, 配列対応版の shared_ptr, などのネタを追加。

 

 

 

 

ライセンス

本ページの情報は、特記無い限り下記 MIT ライセンスで提供されます。

The MIT License (MIT)

Copyright © 2013-2022 Kinoshita Hidetoshi

Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:

The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.

THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.

 

 


参考

 


 

変更履歴

2022-07-13 - ページデザイン更新
2018-05-04 - いろいろアップデート。c++17 に関わる make_shared, 配列対応版の shared_ptr, などのネタを追加。
2014-09-06 - ソースコードを表記を「google-code-prettify」によるものへ変更
2014-03-09 - 追記/修正
2013-12-01 - 新規作成

 

Programming Items トップページ

プライバシーポリシー