В приложении, которое я пишу, я замечаю, что когда я пытаюсь вставить double в базу данных sqlite, (немного) другое значение фактически сохраняется, но только когда приложение выполняется через valgrind. При непосредственном вызове программы (без перекомпиляции) все дубликаты сохраняются как задумано.
Этот код воспроизводит проблему. Некоторые проверки безопасности и тому подобное удалены для краткости.
#include <sqlite3.h>
#include <iostream>
#include <vector>
#include <any>
#include <memory>
#include <cstring>
#include <iomanip>
class SqliteDB
{
sqlite3 *d_db;
bool d_ok;
public:
inline SqliteDB(std::string const &name);
inline ~SqliteDB();
inline void exec(std::string const &q, std::vector<std::vector<std::pair<std::string, std::any>>> *results);
};
inline SqliteDB::SqliteDB(std::string const &name)
:
d_db(nullptr),
d_ok(false)
{
d_ok = (sqlite3_open(name.c_str(), &d_db) == 0);
}
inline SqliteDB::~SqliteDB()
{
if (d_ok)
sqlite3_close(d_db);
}
inline void SqliteDB::exec(std::string const &q, std::vector<std::vector<std::pair<std::string, std::any>>> *results)
{
sqlite3_stmt *stmt;
if (sqlite3_prepare_v2(d_db, q.c_str(), -1, &stmt, nullptr) != SQLITE_OK)
{
std::cout << "SQL Error: " << sqlite3_errmsg(d_db) << std::endl;
return;
}
int rc;
results->clear();
while ((rc = sqlite3_step(stmt)) == SQLITE_ROW)
{
results->resize(results->size() + 1);
for (int i = 0; i < sqlite3_column_count(stmt); ++i)
{
if (sqlite3_column_type(stmt, i) == SQLITE_INTEGER)
{
results->back().emplace_back(std::make_pair(sqlite3_column_name(stmt, i), sqlite3_column_int64(stmt, i)));
}
else if (sqlite3_column_type(stmt, i) == SQLITE_FLOAT)
{
results->back().emplace_back(std::make_pair(sqlite3_column_name(stmt, i), sqlite3_column_double(stmt, i)));
}
else if (sqlite3_column_type(stmt, i) == SQLITE_TEXT)
{
results->back().emplace_back(std::make_pair(sqlite3_column_name(stmt, i), std::string(reinterpret_cast<char const *>(sqlite3_column_text(stmt, i)))));
}
else if (sqlite3_column_type(stmt, i) == SQLITE_BLOB)
{
size_t blobsize = sqlite3_column_bytes(stmt, i);
std::shared_ptr<unsigned char []> blob(new unsigned char[blobsize]);
std::memcpy(blob.get(), reinterpret_cast<unsigned char const *>(sqlite3_column_blob(stmt, i)), blobsize);
results->back().emplace_back(std::make_pair(sqlite3_column_name(stmt, i), std::make_pair(blob, blobsize)));
}
else if (sqlite3_column_type(stmt, i) == SQLITE_NULL)
{
results->back().emplace_back(std::make_pair(sqlite3_column_name(stmt, i), nullptr));
}
}
}
if (rc != SQLITE_DONE)
std::cout << "SQL Error: " << sqlite3_errmsg(d_db) << std::endl;
sqlite3_finalize(stmt);
}
inline std::string toHexString(double d)
{
unsigned char *data = reinterpret_cast<unsigned char *>(&d);
std::ostringstream oss;
oss << "(hex:) ";
for (uint i = 0; i < sizeof(d); ++i)
oss << std::hex << std::setfill('0') << std::setw(2)
<< (static_cast<int32_t>(data[i]) & 0xFF)
<< ((i == sizeof(d) - 1) ? "" : " ");
return oss.str();
}
int main()
{
SqliteDB db(":memory:");
std::vector<std::vector<std::pair<std::string, std::any>>> results;
db.exec("CREATE TABLE part (_id INTEGER PRIMARY KEY, ratio REAL)", &results);
double d = 1.4814814329147339;
std::cout << "Inserting into table: " << std::defaultfloat << std::setprecision(17) << d
<< " " << toHexString(d) << std::endl;
db.exec("INSERT INTO part VALUES (1,1.4814814329147339)", &results);
db.exec("SELECT ratio FROM part WHERE _id = 1", &results);
for (uint i = 0; i < results.size(); ++i)
for (uint j = 0; j < results[i].size(); ++j)
{
if (results[i][j].second.type() == typeid(double))
std::cout << "Retrieved from table: " << std::defaultfloat << std::setprecision(17) << std::any_cast<double>(results[i][j].second)
<< " " << toHexString(std::any_cast<double>(results[i][j].second)) << std::endl;
}
return 0;
}
Я проверил, что проблема возникает при сохранении значения, а не при его получении (то есть база данных фактически содержит различающееся двойное число). Вывод вышеуказанной программы:
[~/valgrindsqlitedouble] $ g++ -std=c++2a -Wall -Wextra -Wshadow -Wold-style-cast -pedantic -fomit-frame-pointer -O1 -g -lsqlite3 main.cc
[~/valgrindsqlitedouble] $ ./a.out
Inserting into table: 1.4814814329147339 (hex:) 00 00 00 e0 25 b4 f7 3f
Retrieved from table: 1.4814814329147339 (hex:) 00 00 00 e0 25 b4 f7 3f
[~/valgrindsqlitedouble] $ valgrind ./a.out
==3340== Memcheck, a memory error detector
==3340== Copyright (C) 2002-2017, and GNU GPL'd, by Julian Seward et al.
==3340== Using Valgrind-3.14.0 and LibVEX; rerun with -h for copyright info
==3340== Command: ./a.out
==3340==
Inserting into table: 1.4814814329147339 (hex:) 00 00 00 e0 25 b4 f7 3f
Retrieved from table: 1.4814814329147341 (hex:) 01 00 00 e0 25 b4 f7 3f
==3340==
==3340== HEAP SUMMARY:
==3340== in use at exit: 0 bytes in 0 blocks
==3340== total heap usage: 299 allocs, 299 frees, 269,972 bytes allocated
==3340==
==3340== All heap blocks were freed -- no leaks are possible
==3340==
==3340== For counts of detected and suppressed errors, rerun with: -v
==3340== ERROR SUMMARY: 0 errors from 0 contexts (suppressed: 0 from 0)
[~/valgrindsqlitedouble] $
Я предполагаю, что происходит некоторая ошибка округления, когда sqlite преобразует строку в удвоенную, но кто-нибудь может объяснить, почему это происходит, только когда работает valgrind?
Во-вторых, я могу избавиться от этой проблемы? В финальной программе точность даже не будет столь важной, но во время разработки было бы хорошо иметь предсказуемый результат. Я работаю с некоторыми большими двоичными файлами, и при тестировании самый простой способ проверить правильность работы программы - сравнить сгенерированный выходной файл (который включает в себя базу данных) с известным хорошим. Есть ли способ заставить sqlite просто вставить 8 байтов, которые я хочу?
Спасибо!