目的
勝手にリソースを解放してくれるような言語やライブラリに慣れた僕にとって、SQLite C/C++ APIを使う際、以下の煩わしさがある。
- 生成した
sqlite3
,sqlite3_stmt
オブジェクトの解放を忘れる。 -
sqlite3
はsqlite3_close()
,sqlite3_stmt
はsqlite3_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()
を利用することで、SQLiteConnector
、SQLiteStmtMaker
それぞれのインスタンスが破棄される順序を気にしなくて良くなる。
まとめ
sqlite3
、sqlite3_stmt
のような、独自の関数で明示的に解放する必要があるリソースを利用する際には、生成・解放を担うクラスを定義し、そのクラスに生成・解放処理を委譲する。
これにより、解放忘れによるリソースリークを回避することができる。
参考文献
本記事の執筆にあたってはAn Introduction To The SQLite C/C++ Interfaceと、そこに挙げられている各関数やオブジェクトのリファレンスを参考にした。