Введение
Представьте: приложение сохраняет важный конфигурационный файл, и в момент записи происходит сбой питания или принудительное завершение процесса. Если запись шла напрямую в целевой файл через QFile, результат — повреждённый, наполовину записанный файл, который потом не сможет прочитать ни одна программа. Для таких сценариев в Qt существует QSaveFile — класс, реализующий атомарную запись.
Концепция
QFile::write() пишет данные непосредственно в целевой файл. Если процесс прерывается на середине операции, файл остаётся в неконсистентном состоянии.
QSaveFile решает эту проблему иначе: данные сначала пишутся во временный файл рядом с целевым, и только при успешном вызове commit() происходит атомарное переименование временного файла в целевое имя (на уровне ОС это операция rename, которая либо выполняется полностью, либо не выполняется вовсе). Если что-то пошло не так — commit() просто не вызывается, и оригинальный файл остаётся нетронутым.
Пример кода: Сравнение QFile, QSaveFile
#include <QFile>
#include <QSaveFile>
#include <QDebug>
#include <QJsonDocument>
#include <QJsonObject>
bool saveConfigUnsafe(const QString &path, const QJsonObject &data)
{
// Небезопасный вариант: при сбое посередине записи
// файл может остаться повреждённым.
QFile file(path);
if (!file.open(QIODevice::WriteOnly))
return false;
QJsonDocument doc(data);
qint64 written = file.write(doc.toJson());
file.close();
return written == doc.toJson().size();
}
bool saveConfigAtomic(const QString &path, const QJsonObject &data)
{
QSaveFile saveFile(path);
// Включаем прямую запись (без промежуточного буфера в памяти),
// что важно для больших файлов.
saveFile.setDirectWriteFallback(true);
if (!saveFile.open(QIODevice::WriteOnly)) {
qWarning() << "Не удалось открыть QSaveFile:" << saveFile.errorString();
return false;
}
QJsonDocument doc(data);
QByteArray json = doc.toJson();
qint64 written = saveFile.write(json);
if (written != json.size()) {
qWarning() << "Ошибка записи данных";
saveFile.cancelWriting(); // явно откатываем, временный файл удалится
return false;
}
// Атомарно переименовывает временный файл в целевой.
// Если commit() не вызван (или вернул false), оригинальный
// файл по пути path остаётся неизменным.
if (!saveFile.commit()) {
qWarning() << "Не удалось завершить атомарную запись:" << saveFile.errorString();
return false;
}
return true;
}
int main()
{
QJsonObject config;
config["version"] = 2;
config["username"] = "qthelper_user";
config["autosave"] = true;
if (saveConfigAtomic("settings.json", config)) {
qDebug() << "Конфигурация сохранена атомарно";
}
return 0;
}
Пояснения к коду
QSaveFile наследует интерфейс QIODevice, поэтому API для записи (write()) не отличается от обычного QFile. Главное отличие — в финальной фазе: вызов commit() обязателен, иначе изменения не применятся, а временный файл будет автоматически удалён при разрушении объекта.
setDirectWriteFallback(true) указывает, что если временный файл не может быть создан (например, нет прав на создание новых файлов в директории, но есть права на запись в существующий), QSaveFile попробует писать напрямую в целевой файл — жертвуя атомарностью ради работоспособности.
Подводные камни
- Забытый
commit(). Самая частая ошибка — записать данные и забыть вызватьcommit(). В этом случае все изменения будут отброшены, а исходный файл останется без изменений, что может маскировать ошибку (программа «молча» не сохраняет данные).
- Права на создание файлов в директории. Атомарное переименование требует, чтобы процесс имел права на создание нового файла в той же директории, где лежит целевой файл (временный файл создаётся рядом). Если у приложения есть только право записи в существующий файл, но не право создания новых —
open()завершится ошибкой, если не включёнsetDirectWriteFallback().
- Атомарность ограничена одной файловой системой. Переименование атомарно только в пределах одного и того же тома/раздела. Если временный каталог находится на другом разделе — гарантии атомарности теряются.
- Сетевые файловые системы. На NFS, SMB и аналогичных сетевых ФС поведение rename может отличаться от локальных дисков, и строгая атомарность не всегда гарантируется производителем сервера.
- Производительность при частых небольших записях. Создание временного файла и rename для каждой мелкой операции дороже, чем прямая запись через
QFile.QSaveFileоправдан для важных данных (конфигурации, документы пользователя), но избыточен для частых логов или кэшей.
- Дисковое пространство. Во время записи временно требуется место и для старого, и для нового файла одновременно — это нужно учитывать при работе с большими файлами на устройствах с ограниченным объёмом памяти.
Выбор между QFile и QSaveFile — это выбор между скоростью и надёжностью. Для пользовательских данных, конфигураций и документов атомарная запись через QSaveFile должна быть стандартом по умолчанию.
================================================================================