通过下面这个非常简单的 C++ 程序(从真实的东西简化)我得到:
test: ./SQLiteCpp-3.3.0/src/Database.cpp:92: void SQLite::Database::Deleter::operator()(sqlite3*): Assertion `0 == ret && "database is locked"' failed.
Aborted (core dumped)
当库尝试在
sqlite3_close()
的析构函数中使用 Database
关闭与数据库的连接时,会发生这种情况。
事实上,
ret
的值为SQLITE_BUSY(5)。 SQLITE 文档表明这通常是连接到同一数据库的另一个进程/线程,但是您可以看到这里的情况并非如此。
main.cpp:
#include "nl.hpp"
int main()
{
auto nl = new NL();
delete nl; // Here only to illustrate the problem.
}
nl.hpp:
#include "SQLiteCpp/SQLiteCpp.h"
using namespace std;
class NL
{
public:
NL();
unique_ptr<SQLite::Statement> sqlst_insert_net;
optional<SQLite::Database> db;
void compile_sql_statements();
void create_db();
};
nl.cpp:
#include <nl.hpp>
NL::NL() {
this->create_db();
this->compile_sql_statements();
}
void NL::create_db() {
this->db.emplace(SQLite::Database(":memory:", SQLite::OPEN_READWRITE | SQLite::OPEN_CREATE));
this->db->exec(
"CREATE TABLE net(id INTEGER PRIMARY KEY, name TEXT UNIQUE);"
);
}
void NL::compile_sql_statements() {
this->sqlst_insert_net = make_unique<SQLite::Statement>(
*(this->db), "INSERT INTO net (name) VALUES (?)");
}
这里似乎起作用的一些事情是:
delete nl;
,问题就不会发生。我不是 C++ 专家,但我想 nl
对象无论如何都会在 main()
结束时被销毁,对吗?this->compile_sql_statements();
的构造函数中删除 NL
就不会出现问题。以上两件事是必要的。这只是一个非常简化的代码,只是为了复制问题。
NL
的实例在实际程序中保存在shared_pointer
中,因此最终必然会调用析构函数。并且还需要准备好的陈述。
我之前使用过看似相同的代码,但没有遇到这个问题。
编辑:附加信息。
这是堆栈跟踪:
SQLite::Database::Deleter::operator()(SQLite::Database::Deleter * const this, sqlite3 * apSQLite) (.../SQLiteCpp-3.3.0/src/Database.cpp:92)
std::unique_ptr<sqlite3, SQLite::Database::Deleter>::~unique_ptr(std::unique_ptr<sqlite3, SQLite::Database::Deleter> * const this) (/usr/include/c++/9/bits/unique_ptr.h:292)
SQLite::Database::~Database(SQLite::Database * const this) (.../SQLiteCpp-3.3.0/include/SQLiteCpp/Database.h:264)
std::_Optional_payload_base<SQLite::Database>::_M_destroy(std::_Optional_payload_base<SQLite::Database> * const this) (/usr/include/c++/9/optional:257)
std::_Optional_payload_base<SQLite::Database>::_M_reset(std::_Optional_payload_base<SQLite::Database> * const this) (/usr/include/c++/9/optional:277)
std::_Optional_payload<SQLite::Database, false, false, false>::~_Optional_payload(std::_Optional_payload<SQLite::Database, false, false, false> * const this) (/usr/include/c++/9/optional:398)
std::_Optional_base<SQLite::Database, false, false>::~_Optional_base(std::_Optional_base<SQLite::Database, false, false> * const this) (/usr/include/c++/9/optional:471)
std::optional<SQLite::Database>::~optional(std::optional<SQLite::Database> * const this) (/usr/include/c++/9/optional:656)
NL::~NL(NL * const this) (.../src/nl.hpp:8)
main() (.../src/test.cpp:5)
以及断言失败的函数(来自 SQLiteCpp 的 Database.cpp):
// Deleter functor to use with smart pointers to close the SQLite database connection in an RAII fashion.
void Database::Deleter::operator()(sqlite3* apSQLite)
{
const int ret = sqlite3_close(apSQLite); // Calling sqlite3_close() with a nullptr argument is a harmless no-op.
// Avoid unreferenced variable warning when build in release mode
(void) ret;
// Only case of error is SQLITE_BUSY: "database is locked" (some statements are not finalized)
// Never throw an exception in a destructor :
SQLITECPP_ASSERT(SQLITE_OK == ret, "database is locked"); // See SQLITECPP_ENABLE_ASSERT_HANDLER
}
正如我提到的,
ret==5 (SQLITE_BUSY)
。
这是包装库:SQLiteC++
编辑:附加信息。
正如评论中所建议的,该语句未正确终止。此处显示的析构函数(其中
sqlite3_finalize(stmt)
所在的位置)从未被调用:
// Prepare SQLite statement object and return shared pointer to this object
Statement::TStatementPtr Statement::prepareStatement()
{
sqlite3_stmt* statement;
const int ret = sqlite3_prepare_v2(mpSQLite, mQuery.c_str(), static_cast<int>(mQuery.size()), &statement, nullptr);
if (SQLITE_OK != ret)
{
throw SQLite::Exception(mpSQLite, ret);
}
return Statement::TStatementPtr(statement, [](sqlite3_stmt* stmt)
{
sqlite3_finalize(stmt);
});
}
TStatementPtr
是存在于Statement
中的shared_pointer。 Statement
的析构函数只是默认的。所以看起来Statement
并没有被破坏。
这个问题恰好很微妙。正如 @Shawn 在问题评论中所建议的,在关闭数据库连接时,准备好的语句尚未最终确定。事实上
Statement
的析构函数应该调用 sqlite3_finalize(stmt)
,但它从未被调用。发生这种情况是因为首先调用了 ~Database
。
这仅仅是因为
Statement
和 Database
在 NL
中声明的顺序。这决定了成员被销毁的顺序。
先声明
Database
,然后Statement
导致Statement
先被销毁,解决问题。