На текущем проекте столкнулся с бардаком в файловых путях: где-то unix-style, где-то windows-style. Пути представлены обычными строками (самописными), формируются динамически, из-за этого например поиск в hash map может фелится, потому-что где-то в легаси код подставляет другие слэши отличные от тех что использовались при записиси hash map и вылезают баги.
Первое, что пришло в голову: отказаться от строк напрямую где они используются в виде файловых путей, а держать все пути в отдельном контейнере. Тогда можно делать sanitize пути в конструкторе соответсвенно платформе и не иметь этих проблем.
Первый набросок:
template<class T, size_t N = 15> class file_path : public inplace_string<T, N> { public: using super = inplace_string<T, N>; static constexpr T separator = #ifdef _WIN32 '\\'; #else '/'; #endif template<size_t M> explicit file_path(const T (&path)[M]) noexcept; bool has_name() const noexcept; bool has_extension() const noexcept; const T *path() const noexcept; const T *name() const noexcept; const T *extension() const noexcept; #ifdef _WIN32 bool has_drive() const noexcept; const T *drive() const noexcept { return drv; } #endif private: const T *blank() const noexcept; #ifdef _WIN32 mutable T drv[3] = {'\0',':','\0'}; #endif size_t ext_off; size_t name_off; };
template<class T, size_t N> template<size_t M> inline file_path<T, N>::file_path(const T ( &path)[M]) noexcept: super( path) { #ifdef _WIN32 super::replace( '/', separator); #else super::replace( '\\', separator); #endif ext_off = super::find_last('.'); name_off = super::find_last(separator, ext_off != super::npos ? M - ext_off - 1 : 0); if (ext_off != super::npos) { // handle "C:\\game.dir\\file" if ((name_off != super::npos) && (ext_off < name_off)) ext_off = super::npos; else ++ext_off; } if (name_off != super::npos) ++name_off; #ifdef _WIN32 if ((super::length() > 1) && (super::at(1) == ':')) { // [A-Za-z]: T ch = super::front(); if (((ch >= 'A') && (ch <= 'Z')) || ((ch >= 'a') && (ch <= 'z'))) { drv[0] = ch; } } #endif // _WIN32 } template<class T, size_t N> inline bool file_path<T, N>::has_name() const noexcept { return name_off != super::npos; } template<class T, size_t N> inline bool file_path<T, N>::has_extension() const noexcept { return ext_off != super::npos; } template<class T, size_t N> inline const T *file_path<T, N>::path() const noexcept { return super::c_str(); } template<class T, size_t N> inline const T *file_path<T, N>::name() const noexcept { return has_name() ? super::begin() + name_off : blank(); } template<class T, size_t N> inline const T *file_path<T, N>::extension() const noexcept { return has_extension() ? super::begin() + ext_off : blank(); } #ifdef _WIN32 template<class T, size_t N> inline bool file_path<T, N>::has_drive() const noexcept { return drv[0] != '\0'; } #endif // _WIN32 template<class T, size_t N> inline const T *file_path<T, N>::blank() const noexcept { if constexpr (std::is_same_v<T, char>) return ""; else return L""; }
Что хорошо в этом подходе, это то что нет лишних копирований или даже аллокаций если мы хотим получить имя файла, расширение или букву диска.
Напишем простой unit test:
int main() { file_path<wchar_t, 32> path( L"C:/games/doom/doom.exe"); std::wcout << path.path( ) << std::endl; std::wcout << path.drive( ) << std::endl; std::wcout << path.name( ) << std::endl; std::wcout << path.extension( ) << std::endl; return 0; }
Вывод:
C:\games\doom\doom.exe
C:
doom.exe
exe
Сделайте приведение к std::filesystem
Пришлось переписать конструктор т. к. первоначальный вариант не обрабатывал правильно тестовые кейсы:
template<class T, size_t N> template<size_t M> inline file_path<T, N>::file_path(const T ( &path)[M]) noexcept: super( path) { if ( !super::empty( )) { #ifdef _WIN32 super::replace( '/', separator); #else super::replace( '\\', separator); #endif ext_off = super::find_last('.'); if (M - 2 == ext_off) // is . the last character? ext_off = super::npos; else if (super::find_last(separator) > ext_off) // do we have a separator past . ? ext_off = super::npos; else ++ext_off; name_off = super::find_last(separator, ext_off != super::npos ? M - ext_off - 1 : 0); if (name_off != super::npos) ++name_off; #ifdef _WIN32 if (super::length() > 1) { T letter = super::operator[](0); if (((letter >= 'A') && (letter <= 'Z')) || ((letter >= 'a') && (letter <= 'z'))) { T delimiter = super::operator[](1); if (':' == delimiter) designator[0] = letter; } } #endif // _WIN32 } }
Решил написать метод, который бы разбивал путь на отдельные токены и возвращал их списком. Т. к. лишних аллокаций делать не хочется, самый простой путь был бы заменить слеши нулями и вернуть указатели на начало токенов. Но это требует mutable контракт, и если контейнер передан в виде const file_path& path то это не получится сделать. Строка внутри вообще может содержать литерал.
Поэтому решил что делаем парсинг во временном буфере, массив токенов валиден до следующего парсинга в этом потоке.
template<class T> class path_buffer { protected: static thread_local T data[256]; }; template<class T, size_t N = 15> class file_path : public inplace_string<T, N>, path_buffer<T> template<class T, size_t N> template<size_t M> inline fixed_vector<const T*, M> file_path<T, N>::tokenize() const noexcept { fixed_vector<const T*, M> tokens; const T *src = super::c_str( ); T* dst = path_buffer<T>::data; size_t i = 0, begin = 0; for ( size_t count = super::length( ); i < count; ++i) { T ch = src[i]; bool split = false; if ( separator == ch) split = true; else if ( ( '.' == ch) && ( i == ext_off - 1)) split = true; if ( !split) [[likely]] dst[i] = ch; else [[unlikely]] { dst[i] = '\0'; if ( i > begin) tokens.push_back( dst + begin); begin = i + 1; } } dst[i] = '\0'; if ( i > begin) tokens.push_back( dst + begin); return tokens; }
Ну и быстро на коленке сварганил свой fixed_vector:
template<class T, size_t N> class fixed_vector { static_assert(std::is_trivially_copyable_v<T>, "fixed_vector requires trivially copyable type"); public: using type = T; using iterator = T*; using const_iterator = const T*; constexpr fixed_vector( ) noexcept = default; constexpr fixed_vector( const std::initializer_list<T>& ls); static constexpr size_t max_size( ) noexcept { return N; } constexpr size_t size( ) const noexcept { return count; } constexpr size_t capacity( ) const noexcept { return N - count; }; constexpr size_t bytes_size( ) const noexcept { return N * sizeof( T); } constexpr bool empty( ) const noexcept { return 0 == count; } constexpr void push_back( const T& value) noexcept; template<class... Ts> constexpr void emplace_back( Ts&&... args); constexpr void pop_back( ) noexcept; constexpr T& front( ) noexcept; constexpr const T& front( ) const noexcept; constexpr T& back( ) noexcept; constexpr const T& back( ) const noexcept; constexpr iterator begin( ) noexcept { return buf; } constexpr iterator end( ) noexcept { return buf + count; } constexpr const_iterator begin( ) const noexcept { return buf; } constexpr const_iterator end( ) const noexcept { return buf + count; } constexpr const_iterator cbegin( ) const noexcept { return begin( ); } constexpr const_iterator cend( ) const noexcept { return end( ); } constexpr T *data( ) noexcept { return buf; } constexpr const T *data( ) const noexcept { return buf; } constexpr T& operator[]( size_t i) noexcept { return buf[i]; } constexpr const T& operator[]( size_t i) const noexcept { return buf[i]; } private: T buf[N]; size_t count = 0; };
file_path<wchar_t, 32> path(L"C:\\games/doom/doom.exe"); auto tokens = path.tokenize<8>( ); for ( auto tok: tokens) std::wcout << tok << std::endl;
Результат:
C:
games
doom
doom
exe
Такой парсинг может быть нужен чтобы например узнать parent dir или список всех директорий.
Если файловые пути только внутренние (задаются разработчиком, а не приходят откуда-то извне), то я бы просто запретил обратный слеш '\\' в них. Прямые слеши под виндами тоже прекрасно работают. А если пути внешние, то там гораздо больше проблем, чем замена разделителя (как насчет поддержки путей типа "\\?\C:\long\path\name.txt").
v1c
> Но это требует mutable контракт, и если контейнер передан в виде const file_path& path то это не получится сделать.
Специально для этого придумали std::string_view.
> Поэтому решил что делаем парсинг во временном буфере, массив токенов валиден до следующего парсинга в этом потоке.
thread_local переменные — это кривой костыль.
}:+()___ [Smile]
> Если файловые пути только внутренние (задаются разработчиком, а не приходят откуда-то извне), то я бы просто запретил обратный слеш '\\' в них.
У нас в движке в одном месте есть path.concat("\\image.png") который просто не работает на Unix-платформе или при поиске в хешмапе если туда положили ресурс с другим separator convention.}:+()___ [Smile]
> А если пути внешние, то там гораздо больше проблем, чем замена разделителя (как насчет поддержки путей типа "\\?\C:\long\path\name.txt").
Нужен полноценный метод path::sanitize() который будет решать все эти проблемы по мере надобности.
boost чем не устраивает?
v1c
> У нас в движке в одном месте есть path.concat("\\image.png") который просто не работает на Unix-платформе или при поиске в хешмапе если туда положили ресурс с другим separator convention.
Ну вот я и говорю, вставить проверку, чтобы на этом сразу падало.
v1c
Boost ?
innuendo
Давно у тебя голоса в голове? Лет 10 назад вроде бы был еще адекватным, нормально общаясь. Сейчас, куда ни загляни - "boost", "urho3d", "убогое апи", "атцы", "зачем тебе сабж", "абырвалг". Сиренькнет что-то на вентилятор одним словом, и сидит довольный.