Czy wiesz, że jesteśmy również na Slacku? Dołącz do nas już teraz klikając tutaj!

Rozmiar oraz atrybuty plików - jak to działa w C++17


2019-02-21, 00:00

Wraz z C++17 dostajemy potężny zestaw typów i funkcji do pracy z systemem plików. O ile wcześniej mogliśmy tworzyć pliki, zapisywać odczytywać z nich dane, to teraz mamy dostęp do tworzenia katalogów, iterowania po nich, obsługi atrybutów i wielu innych zaawansowanych mechanizmów. W artykule zajmiemy się prostym zadaniem: odczytamy rozmiar pliku.

Przed standardem C++17 mogliśmy często narzekać na to, że nawet tak proste z pozoru zadanie jak obliczenie rozmiaru pliku jest skomplikowane. Wraz z std::filesystem staje się to łatwe!!

Odczytywanie wielkości pliku

Przed C++17, Biblioteka Standardowa nie posiadała żadnych bezpośrednich mechanizmów do pracy z systemem plików. Mogliśmy jedynie korzystać z bibliotek zewnętrznych (takich jak Boost) lub używać systemowych API.

Aby pobrać rozmiar pliku, popularną techniką stało się otwarcie pliku, a następnie użycie pozycji wskaźnika w celu odczytania rozmiaru.

Tutaj mamy przykładowy kod używający strumieni:

ifstream testFile("test.file", ios::binary);
const auto begin = myfile.tellg();
testFile.seekg (0, ios::end);
const auto end = testFile.tellg();
const auto fsize = (end-begin);

Inną opcją było otwieranie pliku w trybie dopisywania (std::ios::ate), co powodowało, że wskaźnik automatycznie znajdował się na końcu pliku.

Na Windowsie mogliśmy użyć funkcji GetFileSizeEx:

HANDLE hFile = /* get file/ open/create */

LARGE_INTEGER size;
if (!GetFileSizeEx(hFile, &size))
{
    CloseHandle(hFile);
    return -1; 
}

Nie zbadałem wszystkich możliwych opcji, więc możecie dać mi znać, w jaki sposób Wy radzicie sobie z odczytaniem rozmiaru pliku.

Jak to wygląda w C++17? Czy jest jakaś możliwość na uproszczenie kodu, a może również zagwarantowanie jego przenośności?

Rozmiar pliku z std::filesystem

C++17 przynosi nam std::filesystem do pracy na plikach i katalogach. Nie tylko możemy szybko zdobyć rozmiar pliku oraz jego atrybuty, ale również tworzyć nowe katalogi, iterować po plikach czy operować na obiektach ścieżek.

Nowa biblioteka oferuje nam dwie funkcje, których możemy użyć:

std::uintmax_t std::filesystem::file_size( const std::filesystem::path& p );
std::uintmax_t std::filesystem::directory_entry::file_size() const;

Pierwsza z nich to luźna funkcja zawarta wewnątrz przestrzeni std::filesystem. Druga jest jedną z metod klasy directory_entry.

Każda z nich jest odpowiednio przeładowana. Oznacza to, że możemy zarówno wyrzucać wyjątki, jak i zwracać kod błędu (przez parametr wyjściowy).

Na przykład, możemy zdobyć informację o rozmiarze pliku używając następującego kodu:

try {
    std::filesystem::file_size("test.file");
} catch(fs::filesystem_error& ex) {
    std::cout << ex.what() << '\n';
}

Wersja z kodami błędów:

std::error_code ec{};
auto size = std::filesystem::file_size("a.out", ec);
if (ec == std::error_code{})
    std::cout << "rozmiar: " << size << '\n';
else
    std::cout << "wystąpił błąd w trakcie odczytywania pliku, jego rozmiar: "
              << size << " błąd: " << ec.message() << '\n';

Możecie zapytać, dlaczego mamy aż dwie możliwości wykonania tej operacji: jako luźną funkcję oraz metodę klasy.

Powodem tego jest fakt, że directory_entry przechowuje atrybuty pliku w cache. To dzięki temu, mimo iterowaniu przez katalog lub wielokrotnemu dostępowi do pliku możemy zaoszczędzić na wydajności.

Jeśli plik lub katalog wskazywane przez obiekt directory_entry się zmieni wtedy należy wywołać metodę directory_entry::refresh() aby odświeżyć status parametrów. W innym wypadku metody directory_entry mogą zwracać “stare” wartości.

Prawa dostępu do pliku

Jak wspomnieliśmy wyżej, do pobrania rozmiaru pliku, popularną techniką jest otworzenie pliku, a następnie odczytanie pozycji wskaźnika (za pomocą tellg()). Pierwszym pytaniem, które powinniśmy zadać jest: Co z file permission? Co jeśli nie chcemy otworzyć pliku?

Dzięki std::filesystem nie musimy otwierać pliku, jeżeli tego nie potrzebujemy. Wystarczy przeczytać atrybuty pliku:

Za cppreference: ¹

For a regular file p, returns the size determined as if by reading the st_size member of the structure obtained by POSIX stat (symlinks are followed)

Możemy to szybko sprawdzić, używając bardzo prostego kodu. Stwórzmy przykładowy plik:

std::ofstream sample("hello.txt");
sample << "Hello World!\n";

Możemy przeczytać aktualny zestaw uprawnień i wyświetlić je.

// zaadaptowane z https://en.cppreference.com/w/cpp/filesystem/permissions
void outputPerms(fs::perms p, std::string_view title)
{
    if (!title.empty())
        std::cout << title << ": ";

    std::cout << "właściciel: "
      << ((p & fs::perms::owner_read) != fs::perms::none ? "r" : "-")
      << ((p & fs::perms::owner_write) != fs::perms::none ? "w" : "-")
      << ((p & fs::perms::owner_exec) != fs::perms::none ? "x" : "-");
    std::cout << " grupa: "
      << ((p & fs::perms::group_read) != fs::perms::none ? "r" : "-")
      << ((p & fs::perms::group_write) != fs::perms::none ? "w" : "-")
      << ((p & fs::perms::group_exec) != fs::perms::none ? "x" : "-");
    std::cout << " pozostali: "
      << ((p & fs::perms::others_read) != fs::perms::none ? "r" : "-")
      << ((p & fs::perms::others_write) != fs::perms::none ? "w" : "-")
      << ((p & fs::perms::others_exec) != fs::perms::none ? "x" : "-")
      << '\n';
}

Odczytajmy zatem uprawnienia z naszego nowo utworzonego pliku:

outputPerms(fs::status("hello.txt").permissions());

Otrzymamy (System Linux, Coliru):

owner: rw- group: r-- others: r--

Mamy odpowiednie uprawnienia, więc tellg() zadziała tak, jak się tego spodziewamy:

std::ifstream testFile(std::string("hello.txt"), 
                       std::ios::binary | std::ios::ate);
if (testFile.good())
     std::cout << "tellgSize: " << testFile.tellg() << '\n';
else
    throw std::runtime_error("nie można odczytać pliku...");

Ale jak to się ma do zmiany uprawnień, kiedy nie możemy otworzyć pliku do czytania, zapisywania lub wykonywania?

fs::permissions(sTempName, fs::perms::owner_all,
                fs::perm_options::remove);
outputPerms(fs::status(sTempName).permissions());

co pokaże:

owner: --- group: r-- others: r--

fs::permissions to metoda, która pozwala na zmianę uprawnień. Jako pierwszy parametr podajemy uprawnienie (będące maską bitową), a następnie “operację”: remove, replace lub add.

W naszym przypadku chcę usunąć wszystkie uprawnienia dla właściciela pliku.

Stała perms::owner_all jest kompozycją owner_read | owner_write | owner_exec.

Teraz… spróbujmy wykonać ten sam kod używający tellg()… Otrzymamy:

general exception: nie można odczytać pliku...

W takim razie co z fs::file_size?:

auto fsize = fs::file_size(sTempName);
std::cout << "fsize: " << fsize << '\n';

Otrzymamy, tak jak zakładaliśmy:

fsize: 13

Niezależnie od uprawnień pliku, możemy odczytać jego rozmiar.

Oto drobne DEMO: @Coliru

Dostęp do katalogu nadrzędnego

O ile nie potrzebujemy uprawnień do czytania/zapisywania/uruchamiania pliku, to potrzebujemy prawa do katalogu nadrzędnego.

Zrobiłem jeden dodatkowy test, w którym usunąłem wszystkie prawa z katalogu "." (miejsca, w którym tworzymy plik):

fs::permissions(".", fs::perms::owner_all,
                     fs::perm_options::remove);

auto fsize = fs::file_size(sTempName);
std::cout << "fsize: " << fsize << '\n';

Ale otrzymałem:

filesystem error! filesystem error:
cannot get file size: Permission denied [hello.txt]

Możesz uruchomić ten kod używając Coliru

Notka dla Windowsa

Windows nie jest systemem z rodziny POSIX, więc nie obsługuje on formatu uprawnień zgodnych z jego schematem. Dla std::filesystem system Windows wspiera tylko dwa tryby:

  • (read/write) - odczyt, zapis i wykonywanie - wszystkie tryby
  • (read-only) - odczyt, wykonywanie - wszystkie tryby

To dlatego nasz kod demo nie zadziała. Zabranie praw do czytania pliku nie wpłynie tutaj na nic.

Wydajność

Pobieranie wielkości pliku może nie jest kluczowym punktem większości aplikacji, ale od kiedy jesteśmy programistami C++, to chcemy wiedzieć, co jest szybsze… prawda?

Dopóki nie ma potrzeby odczytywać zawartości pliku, funkcja std::filesystem wydaje się szybszą… nie prawda?

Co więcej, metoda directory_entry mogłaby być nawet szybsza dzięki możliwości cache-owania wyników dla pojedynczej ścieżki. Jeżeli planujecie uzyskiwać dostęp do tych informacji wielokrotnie, to mądrzej będzie użyć directory_entry.

Mam prosty test (podziękowania dla Patrice Roy za kod). Możesz uruchomić go używając @Coliru.

Test uruchamia się N = 10'000 razy.

Uruchomiony na Coliru (Linux):

filesystem::file_size     : 2543920 in 21 ms.
homemade file_size        : 2543920 in 66 ms.
directory_entry file_size : 2543920 in 13 ms.

Uruchomiony na Windows-ie:

PS .\Test.exe
filesystem::file_size     : 1200128 in 81 ms.
homemade file_size        : 1200128 in 395 ms.
directory_entry file_size : 1200128 in 0 ms.

PS .\Test.exe
filesystem::file_size     : 1200128 in 81 ms.
homemade file_size        : 1200128 in 390 ms.
directory_entry file_size : 1200128 in 0 ms.

Interesujące jest, że metoda directory_entry jest bezkonkurencyja w porównaniu do innych technik. Jednakże, nie zmierzyłem czasu dostępu pierwszego pobrania wartości.

Podsumowanie

W tym wpisie zobaczyliśmy, w jaki sposób używać funkcji file_size z przestrzeni std::filesystem. Zachęcam Was do badania tego nowego dodatku do Standardu C++17. Jeżeli pracujecie na codzień z plikami i katalogami, to tworzenie kodu za jego pomocą będzie bardziej komfortowe, a sam kod będzie przenośny.

Tutaj małe demo na Coiluru: DEMO.

Wiecej o C++ na blogu Bartka: bfilipek.com


¹ - Treść pozostawiona w formie oryginalnej



Bartłomiej Filipek

Programista i pasjonat C++ z ponad 11-letnim doświadczeniem. Bloguje od wielu lat, głównie o naszym ulubionym języku programowania. Autor ksiązki C++17 In Detail.

Blog Bartka
Profil na LinkedIn
Pssst! Używamy Cookies. Poprzez używanie naszego serwisu zgadzasz się na odczytywanie i zapisywanie Cookies w swojej przeglądarce.