2
4

More than 5 years have passed since last update.

【SQLite C/C++ API】sqlite3, sqlite3_stmtオブジェクトの解放忘れを防ぐ(C++)

Posted at

目的

勝手にリソースを解放してくれるような言語やライブラリに慣れた僕にとって、SQLite C/C++ APIを使う際、以下の煩わしさがある。

  • 生成したsqlite3, sqlite3_stmt オブジェクトの解放を忘れる。
  • sqlite3sqlite3_close(), sqlite3_stmtsqlite3_finalize()でそれぞれ解放する必要があるが、間違えてsqlite3_free()のようなメモリ解放関数にsqlite3_stmtオブジェクトを渡してランタイムエラーを起こしてしまうことがある。

上記問題を解決するために、sqlite3, sqlite3_stmtオブジェクトの生成をクラスに移譲し、そのクラスのインスタンス破棄時に、生成したオブジェクトを自動的に破棄するようにする。

クラスの定義と利用

sqlite3の生成、解放を担うSQLiteConnectorクラスを以下のように定義する。

class SQLiteConnector
{
public:
    // filePathはUTF-8で渡すこと
    SQLiteConnector(const std::string& filePath)
        : m_filePath(filePath) {}

    ~SQLiteConnector()
    {
        for (int i = 0, ii = m_dbs.size(); i < ii; i++)
        {
            sqlite3 *db = m_dbs.at(i);
            if (db != nullptr)
            {
                sqlite3_close_v2(db);
            }
        }
    }

    sqlite3 *open()
    {
        int rc;
        sqlite3 *db = nullptr;
        rc = sqlite3_open(m_filePath.c_str(), &db);

        if (rc != SQLITE_OK)
        {
            // エラーハンドリング
        }
        m_dbs.push_back(db);
        return db;
    }

protected:
    std::string m_filePath;
    std::vector<sqlite3*> m_dbs;
};

以下のように利用する。

void useConnector()
{
    std::string filePath = "test.sqlite";
    SQLiteConnector connector(filePath);
    sqlite3 *db = connector.open();

    // *dbを使った処理

    // この関数内でsqlite3_close()を呼び出す必要はない。
}

関数useConnector()のスコープから抜ける時点で、SQLiteConnectorインスタンスは破棄される。
破棄される際のデストラクタで、生成されたsqlite3オブジェクトは開放される。
従って、useConnector()内でsqlite3_close()sqlite3_close_v2()を呼び出す必要はない。

sqlite3_stmtについても、同様の生成、破棄処理を担うクラスを定義できる。


class SQLiteStmtMaker
{
public:
    SQLiteStmtMaker(sqlite3 *db)
        : m_db(db) {}

    ~SQLiteStmtMaker()
    {
        for (int i = 0, ii = m_stmts.size(); i < ii; i++)
        {
            sqlite3_stmt *stmt = m_stmts.at(i);
            if (stmt != nullptr)
            {
                sqlite3_finalize(stmt);
            }
        }
    }

    sqlite3_stmt *prepare(const std::string& sql)
    {
        int rc;
        sqlite3_stmt* stmt = nullptr;
        rc = sqlite3_prepare_v2(m_db, sql.c_str(), -1, &stmt, nullptr);
        if (rc != SQLITE_OK)
        {
            // エラーハンドリング
        }
        m_stmts.push_back(stmt);
        return stmt;
    }

protected:
    sqlite3 *m_db;  // このクラスで解放しない
    std::vector<sqlite3_stmt*> m_stmts; // このクラスで解放する
};

以下のように利用する。

void useStmtMaker()
{
    std::string filePath = "test.sqlite";
    SQLiteConnector connector(filePath);
    sqlite3 *db = connector.open();
    SQLiteStmtMaker maker(db);

    sqlite3_stmt *stmt = maker.prepare("CREATE TABLE IF NOT EXISTS t1(c1 TEXT)");

    int rc = sqlite3_step(stmt);
    if (rc != SQLITE_DONE)
    {
        // エラーハンドリング
    }

    // この関数内でsqlite3_finalize()を呼び出す必要はない
}

useStmtMaker()関数から抜けるタイミングで、生成されたsqlite3オブジェクト、およびsqlite3_stmtオブジェクトは自動的に解放される。
これで解放忘れによるリソースリークの悪夢から逃れられる。

注記:sqlite3の解放にはsqlite3_close_v2()を利用する

SQLiteConnectorのデストラクタでは、sqlite3_close()ではなくsqlite3_close_v2()を利用してsqlite3オブジェクトを解放する。

sqlite3_close()を利用して解放する場合、解放しようとするsqlite3と関連付けられたsqlite3_stmtが全て解放されていないと、sqlite3_close()SQLITE_BUSYを返し、sqlite3が解放されない。
すなわち、必ずsqlite3_finalize() -> sqlite3_close() の順で呼び出す必要がある。

一方で、sqlite3_close_v2()を利用すると、sqlite3と関連付けられたsqlite3_stmtのうちで解放されていないものがあったとしても、エラーとならない。関連付けられたsqlite3_stmtがすべて解放された時点で、sqlite3も解放される。
すなわち、sqlite3_finalize()sqlite3_close_v2()の呼び出しは順不同で良い。

よって、sqlite3_close_v2()を利用することで、SQLiteConnectorSQLiteStmtMakerそれぞれのインスタンスが破棄される順序を気にしなくて良くなる。

まとめ

sqlite3sqlite3_stmtのような、独自の関数で明示的に解放する必要があるリソースを利用する際には、生成・解放を担うクラスを定義し、そのクラスに生成・解放処理を委譲する。
これにより、解放忘れによるリソースリークを回避することができる。

参考文献

本記事の執筆にあたってはAn Introduction To The SQLite C/C++ Interfaceと、そこに挙げられている各関数やオブジェクトのリファレンスを参考にした。

2
4
2

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
2
4