Zadanie

Znajdź i usuń błąd w następującym fragmencie kodu źródłowego:

#include <cstdio>
#include <string>

#define MAX_LEN 512

int main()
{
   std::string imageFileName = "/image/file/name";
    
   // ...
    
   if (int len = imageFileName.size() >= MAX_LEN)
   {
      printf("File path %s is too long (max len: %d): %d\n",
             imageFileName.c_str(), MAX_LEN, len);
      
      return EXIT_FAILURE;
   }
   
   printf("File path %s is valid\n", imageFileName.c_str());
   
   // ...
}

Uruchom w edytorze

File path /image/file/name is valid

Rozwiązanie

Przedstawiony fragment kodu źródłowego waliduje czy długość ścieżki do pliku, zawartej w obiekcie imageFileName typu std::string (plik nagłówkowy string) jest mniejsza od długości reprezentowanej przez makto MAX_LEN (równe co do wartości 512). W przeciwnym wypadku program kończy działanie wyświetlając kolejno: nazwę pliku, wartość makra  MAX_LEN oraz aktualną długość ścieżki do pliku, która w takim przypadku powinna być większa lub równa 512 (spełniony zostanie warunek dla instrukcji if) . Na pierwszy rzut oka wszystko wygląda w porządku jednak tak nie jest.

Na początku zmieńmy rozmiar łańcucha tekstowego reprezentującego ścieżkę do pliku na wartość graniczną reprezentowaną przez makro MAX_LEN. Zawartość tej ścieżki nie ma dla nas znaczenia, dlatego nie musimy wymyślać tutaj długiego łańcucha tekstowego. Aby sprawdzić czy dla obecnej instrukcji if spełniony zostanie warunek, wystarczy że zmienimy rozmiar dla obiektu  imageFileName za pomocą metody std::string::resize:

#include <cstdio>
#include <string>

#define MAX_LEN 512

int main()
{
   std::string imageFileName = "/image/file/name";
   
   imageFileName.resize(MAX_LEN);
    
   // ...
    
   if (int len = imageFileName.size() >= MAX_LEN)
   {
      printf("File path is too long (max len: %d): %d\n", MAX_LEN, len);
      
      return EXIT_FAILURE;
   }
   
   printf("File path is valid\n");
   
   // ...
}

Uruchom w edytorze

File path /image/file/name is too long (max len: 512): 1

Zgodnie z oczekiwaniami warunek dla instrukcji if został spełniony. Inaczej jest w przypadku outputu funkcji printf, który informuje nas, że długość ścieżki do pliku jest równa ”1”, natomiast spodziewaną wartością jest ”512”. Jest to objaw poważnego błędu logicznego, natomiast warunek dla instrukcji if został spełniony przez przypadek. Gdyby nie output z funkcji printf, błąd taki mógłby pozostać niezauważony w naszym kodzie źródłowym przez długi czas.

Problem w przedstawionym kodzie tkwi w priorytetach operatorów (ang. precedence): =” (operator podstawienia). oraz  ”>=” (operator większy lub równy). Operator>=” ma wyższy priorytet niż operator=” co oznacza iż nasz kod mógłby wyglądać tak (zwróć uwagę na dodatkowe nawiasy w warunku dla instrukcji if):

#include <cstdio>
#include <string>

#define MAX_LEN 512

int main()
{
   std::string imageFileName = "/image/file/name";
   
   imageFileName.resize(MAX_LEN);
    
   // ...
    
   if (int len = (imageFileName.size() >= MAX_LEN))
   {
      printf("File path is too long (max len: %d): %d\n", MAX_LEN, len);
      
      return EXIT_FAILURE;
   }
   
   printf("File path is valid\n");
   
   // ...
}

Uruchom w edytorze

File path /image/file/name is too long (max len: 512): 1

Okazuje się więc, że najpierw wyznaczany jest wynik takiego wyrażenia:

imageFileName.size() >= MAX_LEN

Wynik tego wyrażenia ma typ logiczny bool, który w przedstawionym przypadku ma wartość true, ponieważ długość naszej ścieżki istotnie jest większa lub równa MAX_LEN. Tak powstała wartość logiczna jest konwertowana do typu int jako ”1” ze względu na przypisanie do docelowej zmiennej len (typu int). Wartość niezerowa traktowana jest jak true, więc ciało instrukcji if zostanie wykonane (przez przypadek). Analogicznie jest w przypadku gdy długość ścieżki do pliku jest krótsza od MAX_LEN – w efekcie otrzymujemy wartość ”0”, która traktowana jest jak false, a więc ciało instrukcji if nie zostanie wykonane (przykład na początku artykułu).

Aby rozwiązać ten problem nie możemy zmienić położenia nawiasów w taki sposób:

#include <cstdio>
#include <string>

#define MAX_LEN 512

int main()
{
   std::string imageFileName = "/image/file/name";
    
   imageFileName.resize(MAX_LEN); 
       
   // ...
    
   if ((int len = imageFileName.size()) >= MAX_LEN)
   {
      printf("File path %s is too long (max len: %d): %d\n",
             imageFileName.c_str(), MAX_LEN, len);
      
      return EXIT_FAILURE;
   }
   
   printf("File path %s is valid\n", imageFileName.c_str());
   
   // ...
}
main.cpp: In function ‘int main()’:
main.cpp:14:9: error: expected primary-expression before ‘int’
    if ((int len = imageFileName.size()) >= MAX_LEN)
         ^~~
main.cpp:14:9: error: expected ‘)’ before ‘int’
main.cpp:22:4: error: expected ‘)’ before ‘printf’
    printf("File path %s is valid\n", imageFileName.c_str());
    ^~~~~~

Przenieśmy zatem przypisanie długości ścieżki do pliku do zmiennej len na zewnątrz instrukcji if. Dodatkowo zamiast typu int dla zmiennej len zastosujmy słowo kluczowe auto dzięki czemu otrzymamy typ (std::string::size_type, którym często jest typ size_t) odpowiedni do przechowywania długości łańcucha tekstowego:

auto len = imageFileName.size();    
if (len >= MAX_LEN)
{
   // ...
}

Pozwoli to na uniknięcie kolejnego błędu, który mógłby się pojawić w przypadku długości ścieżki do pliku większej niż maksymalna wartość dla typu przechowywanego w typie zmiennej len. Aby to zobrazować zastąpmy słowo kluczowe auto typem uint8_t:

#include <cstdio>
#include <string>

#define MAX_LEN 512

int main()
{
   std::string imageFileName = "/image/file/name";
    
   imageFileName.resize(MAX_LEN); 
       
   // ...
    
   uint8_t len = imageFileName.size();    
   if (len >= MAX_LEN)
   {
      printf("File path %s is too long (max len: %d): %d\n",
             imageFileName.c_str(), MAX_LEN, len);
      
      return EXIT_FAILURE;
   }
   
   printf("File path %s is valid\n", imageFileName.c_str());
   
   // ...
}

Uruchom w edytorze

File path /image/file/name is valid

Warto także zastąpić makro MAX_LEN (znajdujący się tam literał ma typ int podobnie jak początkowy typ zmiennej len), stałym wyrażeniem czasu kompilacji (constexpr), przyjmując jednocześnie bezpieczniejszy typ size_t:

constexpr size_t MAX_LEN = 512;

Na zakończenie, zmienimy symbole formatujące w wywołaniu funkcji printf z „%d” (odpowiednie dla typu int) na ”%lu”, tak aby odnosiły się do 64-bitowych wartości całkowitych bez znaku (odpowiednie dla typu std::string::size_type oraz size_t):

auto len = imageFileName.size();    
if (len >= MAX_LEN)
{
   printf("File path %s is too long (max len: %lu): %lu\n",
          imageFileName.c_str(), MAX_LEN, len);
      
   return EXIT_FAILURE;
}

Po uwzględnieniu powyższych uwag otrzymujemy następujący (naprawiony) kod źródłowy:

#include <cstdio>
#include <string>

constexpr size_t MAX_LEN = 512;

int main()
{
   std::string imageFileName = "/image/file/name";
    
   imageFileName.resize(MAX_LEN); 
       
   // ...
    
   auto len = imageFileName.size();    
   if (len >= MAX_LEN)
   {
      printf("File path %s is too long (max len: %lu): %lu\n",
             imageFileName.c_str(), MAX_LEN, len);
      
      return EXIT_FAILURE;
   }
   
   printf("File path %s is valid\n", imageFileName.c_str());
   
   // ...
}

Uruchom w edytorze

File path /image/file/name is too long (max len: 512): 512

Zadanie wykonane, choć warto zwrócić uwagę na jeden powstały problem, Otóż z założenia zakres dla zmiennej len miał być ograniczony wyłącznie do instrukcji if. Naprawiając powyższy kod źródłowy przenieśliśmy inicjalizację tej zmiennej przed instrukcję if, zwiększając być może niepotrzebnie jej zakres. Problem ten rozwiązuje własność, która pojawiła się w standardzie C++17, umożliwiająca początkową inicjalizację zmiennych w ramach instrukcji if (podobnie jak w przypadku standardowej pętli for). Dzięki temu możemy ograniczyć zakres naszej zmiennej len wyłącznie do instrukcji if:

#include <cstdio>
#include <string>

constexpr size_t MAX_LEN = 512;

int main()
{
   std::string imageFileName = "/image/file/name";
    
   imageFileName.resize(MAX_LEN); 
       
   // ...
    
   if (auto len = imageFileName.size(); len >= MAX_LEN)
   {
      printf("File path %s is too long (max len: %lu): %lu\n",
             imageFileName.c_str(), MAX_LEN, len);
      
      return EXIT_FAILURE;
   }
   
   printf("File path %s is valid\n", imageFileName.c_str());
   
   // ...
}

Uruchom w edytorze

File path /image/file/name is too long (max len: 512): 512