c++filemutexofstream

Ofstream-based class overwrites files


I have created a "File" class for a C++ project of mine, and its based on ofstream. Here is the simplified code:

file.h

#include <fstream>
#include <mutex>
#include <filesystem>
#include <vector>

class File
{
private:
  std::ofstream writer;
  static std::mutex file_mtx;

  std::filesystem::path path;

public:
  File();
  File(const std::filesystem::path &_path);

  void write(const std::vector<std::string> &lines);
};

file.cpp

File::File(const std::filesystem::path &_path) : path(_path)
{
    // Create file
    std::lock_guard<std::mutex> lock(file_mtx);
    writer.open(path, std::ios::app);
    writer.close();
}

void File::write(const std::vector<std::string> &lines)
{
    std::lock_guard<std::mutex> lock(file_mtx);
    writer.open(path, std::ios::app);

    if (!writer.is_open())
        return;

    for (const auto &line : lines)
        writer << line
               << "\n";
    
    writer.close();
}

std::mutex File::file_mtx;

I open the file with ofstream only when needed to be more efficient, and I use a mutex to prevent the same file being written at the same time by two different objects. The write function also opens in append mode (std::ios::app).

Here is my "test" code:

#include "file.h"

int main(int argc, char *argv[])
{
  File test("./test.txt");
  test.write({
    "Hello",
    "World"
  });

  File test2("./test.txt");
  test2.write({
    "Part",
    "2"
  });

  return 0;
}

The reason I have two different variables for the same file is because in my actual code, the file must be accessed in two different scopes, so two variables must be defined.

Here is the output for the file:

Part
2

and here is the expected output:

Hello
World
Part,
2

Despite the mutex being locked at the beginning of the function, and despite the file being opened in append mode, the file still gets overwritten. I was wondering why this is happening, and why the mutex doesn't stop the file from getting overwritten in the first place.

Here is my minimal reproducible example:

#include <fstream>
#include <mutex>
#include <filesystem>
#include <vector>

class File
{
private:
  std::ofstream writer;

  std::filesystem::path path;

public:
  File(const std::filesystem::path &_path) : path(_path)
  {
    // Create file
    writer.open(path, std::ios::app);
    writer.close();
  }

  void write(const std::vector<std::string> &lines)
  {
    if (!writer.is_open())
      return;

    for (const auto &line : lines)
      writer << line
             << "\n";

    writer.close();
  }
};

int main(int argc, char *argv[])
{
  File test("./test.txt");
  test.write({"Hello",
              "World"});

  File test2("./test.txt");
  test2.write({"Part",
               "2"});

  return 0;
}

Solution

  • Your issue is not related to the mutex. In the code you originally posted, the file is recreated in the constructor:

    File::File(const std::filesystem::path& _path) : path(_path)
    {
      // Create file
      std::lock_guard<std::mutex> lock(file_mtx);
      writer.open(path); // this clears the file content
      writer.close();
    }
    

    You then replaced the above code with:

    File(const std::filesystem::path& _path) : path(_path)
    {
      // Create file
      writer.open(path, std::ios::app); // this does not clear the file content
      writer.close();
    }
    

    This code does not recreate the file but will not yield the expected result, as the file is opened in append mode. Consequently, each time you run the program, the text will be appended:

    Hello
    World
    Part
    2
    Hello
    World
    Part
    2...
    

    What you probably want is a reset method:

    File(const std::filesystem::path& _path) : path(_path)
    {
      // no code here
    }
    
    void reset()
    {
      // the code you originally placed in constructor 
    }
    
    int main(int argc, char* argv[])
    {
      File("./test.txt").reset();
    
      // ...
    }