Pull to refresh

Пути к файлам

Reading time 4 min
Views 67K
Казалось бы — что может быть проще, чем работа с файлами в C++. Но отдельные личности поражают своей находчивостью в поиске наихудшего подхода.
Не стоит делать так:

std::string filepath("C:\\тест");
std::ofstream file(filepath.c_str());



Если кратко, то использование не ASCII символов в строковых константах char может привести к печальным последствиям. Я уже обсуждал этот вопрос в посте о кодировках. В данном случае название файла напрямую зависит от кодировки исходника и если кто-то напишет подобное в utf-8, в windows-xp можно получить файл с запрещенными символами, с которым невозможно будет ничего сделать. Можно не использовать не ASCII. Но вы же не можете запретить это пользователю (потоку или БД из которого получен путь). Это же дискриминация по национальному признаку! Срочно исправляемся:

std::wstring filepath= L"C:\тест"
std::ofstream file(filepath.c_str());


Несведущие в стандарте пользователи Visual Studio могут успокоиться, пока нужда не заставит сменить компилятор (точнее STL). И тут начинается…
— «дурацкий gcc» или «дурацкий stlport» не содержит конструктор ofstream::ofstream(wchar_t*)

Дело в том, что текущий стандарт и не предполагает его наличия (не трогаем пока C++0x). Это в чистом виде энтузиазм мелкомягких.

Что же делать?



Вариантов несколько
  • Использовать стороннюю библиотеку для работы с путями (к примеру boost::filesystem)
  • Использовать std::locale
  • Придумывать свой


С третьим вариантом все ясно, с первым тоже ничего сложного:
  1.  
  2. #include <boost/filesystem/fstream.hpp>
  3. #include <string>
  4.  
  5. namespace fs = boost::filesystem;
  6.  
  7. int main(int argc, char* argv)
  8. {
  9.   std::wstring filepath(L"C:\тест");
  10.   fs::ofstream(filepath);
  11.   return 0;
  12. }
  13.  

А вот по поводу второго у тех, кто не учил матчасть, могут возникнуть проблемы.

Пользуемся std::locale



Отступление.
Огорчу пользователей mingw: вам придется использовать стороннюю реализацию STL (к примеру stlport) из-за отсутствия в родной правильной поддержки локализации. А точнее, функция std::locale("") всегда возвращает std::locale(«C»), что бы там у вас не стояло. Тот же stlport лишен подобного недостатка. О том как слепить связку mingw+stlport+boost я отписал тут.

Все что нам нужно сделать это следовать простым правилам — с не ASCII работаем в «расширенном» виде. То есть, читаем путь в std::wstring, используя соответствующим образом локализованный поток, а при использовании, сужаем по пользовательской локализации. Эта идея основана на том, что раз пользователь правильно видит символы своего языка в консоли, то его пользовательская локализация знает в какую кодировку надо сузить широкую строку, чтобы правильно интерпретировать путь. Итак, пример. Допустим у нас есть файл в кодировке cp866, содержащий путь. Нам необходимо создать файл по этому пути. Что мы делаем:
  1.  
  2. #include <iostream>
  3. #include <string>
  4. #include <fstream>
  5. #include <locale>
  6. #include <memory>
  7. #include "facet/codecvt/codecvt_cp866.hpp"
  8.  
  9. /**@brief Сужает широкую строку, используя локализацию loc
  10.    @return Возвращает суженную строку или пустую суженную строку, в
  11.    случае. если возникла ошибка*/
  12. std::string narrow(const std::wstring& wstr, const std::locale& loc)
  13. {
  14.   const size_t sz = wstr.length();
  15.   if(sz == 0)
  16.     return std::string();
  17.   mbstate_t state = 0;
  18.   char *cnext;
  19.   const wchar_t *wnext;
  20.   const wchar_t *wcstr = wstr.c_str();
  21.   char *buffer = new char[sz + 1];
  22.   std::uninitialized_fill(buffer, buffer + sz + 1, 0);
  23.   typedef std::codecvt<wchar_t, char, mbstate_t> cvt;
  24.   cvt::result res;
  25.   res = std::use_facet<cvt>(loc).out(state, wcstr, wcstr + sz, wnext,
  26.       buffer, buffer + sz, cnext);
  27.   std::string result(buffer);
  28.   if(res == cvt::error)
  29.     return std::string();
  30.   return result;
  31. }
  32.  
  33. /**@brief Расширяет строку, используя локализацию loc
  34.    @return Возвращает расширенную строку или пустую расширенную строку, в
  35.    случае, если возникла ошибка.*/
  36. std::wstring widen(const std::string& str, const std::locale& loc)
  37. {
  38.   const size_t sz = str.length();
  39.   if(sz == 0)
  40.     return std::wstring();
  41.   mbstate_t state = 0;
  42.   const char *cnext;
  43.   wchar_t *wnext;
  44.   const char *cstr = str.c_str();
  45.   wchar_t *buffer = new wchar_t[sz + 1];
  46.   std::uninitialized_fill(buffer, buffer + sz + 1, 0);
  47.   typedef std::codecvt<wchar_t, char, mbstate_t> cvt;
  48.   cvt::result res;
  49.   res = std::use_facet<cvt>(loc).in(state, cstr, cstr + sz, cnext,
  50.       buffer, buffer + sz, wnext);
  51.   std::wstring result(buffer);
  52.   delete [] buffer;
  53.   if(res == cvt::error)
  54.     return std::wstring();
  55.   return result;
  56. }
  57.  
  58. int main(int argc, char* argv[])
  59. {
  60.   //Пусть имеется cp866 файл с путем
  61.   std::ofstream ofile("input.txt", std::ios::binary);
  62.   if(!ofile)
  63.   {
  64.     std::cerr << "Error open file" << std::endl;
  65.     return 0;
  66.   }
  67.   std::ostreambuf_iterator<char> writer(ofile);
  68.   *(writer)   = 0xe2; // т
  69.   *(++writer) = 0xa5; // е
  70.   *(++writer) = 0xe1; // с
  71.   *(++writer) = 0xe2; // т
  72.   ofile.close();
  73.  
  74.   //Читаем путь
  75.   std::locale cp866(std::locale(), new codecvt_cp866);
  76.   std::wifstream ifile("input.txt", std::ios::binary);
  77.   ifile.imbue(cp866);
  78.   std::wstring wpath;
  79.   ifile >> wpath;
  80.   ifile >> wpath;
  81.   ifile.close();
  82.  
  83.   //Создаем по этому пути файл
  84.   std::ofstream file(narrow(wpath, std::locale("")).c_str());
  85.   file << "testing";
  86.   file.close();
  87. }
  88.  


Фасеты можно взять на git-hub.

SUMMARY



Если вы используете путь из argv — можете смело с ним работать (пользователь знает что делает). Из «внешней среды» путь получайте с помощью правильно локализованного потока как широкую строку и сужайте ее с помощью пользовательской локализации.

С вопросами можно обращаться:
0. К стандарту
1. К книге Страуструпа (3-е специальное издание, приложение)
2. К документации по mingw.
3. К документации по boost.
4. К посту о фасетах и кодировках.

Всем прямых путей!

UPD: Ну и как правильно заметили Gorthauer87,Migun и naryl в комментариях, обратные слеши и платформо-специфичные пути тоже плохая идея.
Tags:
Hubs:
+41
Comments 49
Comments Comments 49

Articles