ПрограммированиеФорумОбщее

Пишем свой file_path

Страницы: 1 2 3 4 Следующая »
#0
16:46, 17 янв 2026

На текущем проекте столкнулся с бардаком в файловых путях: где-то unix-style, где-то windows-style. Пути представлены обычными строками (самописными), формируются динамически, из-за этого например поиск в hash map может фелится, потому-что где-то в легаси код подставляет другие слэши отличные от тех что использовались при записиси hash map и вылезают баги.

Первое, что пришло в голову: отказаться от строк напрямую где они используются в виде файловых путей, а держать все пути в отдельном контейнере. Тогда можно делать sanitize пути в конструкторе соответсвенно платформе и не иметь этих проблем.

#1
16:47, 17 янв 2026

Первый набросок:

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;
};
#2
16:48, 17 янв 2026
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"";
}

Что хорошо в этом подходе, это то что нет лишних копирований или даже аллокаций если мы хотим получить имя файла, расширение или букву диска.

#3
16:53, 17 янв 2026

Напишем простой 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

#4
0:15, 18 янв 2026

Сделайте приведение к std::filesystem

#5
15:24, 18 янв 2026

Пришлось переписать конструктор т. к. первоначальный вариант не обрабатывал правильно тестовые кейсы:

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
    }
}
#6
15:29, 18 янв 2026

Решил написать метод, который бы разбивал путь на отдельные токены и возвращал их списком. Т. к. лишних аллокаций делать не хочется, самый простой путь был бы заменить слеши нулями и вернуть указатели на начало токенов. Но это требует 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;
}
#7
15:31, 18 янв 2026

Ну и быстро на коленке сварганил свой 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;
};
#8
15:35, 18 янв 2026
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 или список всех директорий.

#9
6:57, 19 янв 2026

Если файловые пути только внутренние (задаются разработчиком, а не приходят откуда-то извне), то я бы просто запретил обратный слеш '\\' в них. Прямые слеши под виндами тоже прекрасно работают. А если пути внешние, то там гораздо больше проблем, чем замена разделителя (как насчет поддержки путей типа "\\?\C:\long\path\name.txt").

v1c
> Но это требует mutable контракт, и если контейнер передан в виде const file_path& path то это не получится сделать.
Специально для этого придумали std::string_view.

> Поэтому решил что делаем парсинг во временном буфере, массив токенов валиден до следующего парсинга в этом потоке.
thread_local переменные — это кривой костыль.

#10
13:40, 19 янв 2026

}:+()___ [Smile]
> Если файловые пути только внутренние (задаются разработчиком, а не приходят откуда-то извне), то я бы просто запретил обратный слеш '\\' в них.
У нас в движке в одном месте есть path.concat("\\image.png") который просто не работает на Unix-платформе или при поиске в хешмапе если туда положили ресурс с другим separator convention.}:+()___ [Smile]
> А если пути внешние, то там гораздо больше проблем, чем замена разделителя (как насчет поддержки путей типа "\\?\C:\long\path\name.txt").
Нужен полноценный метод path::sanitize() который будет решать все эти проблемы по мере надобности.

#11
18:19, 19 янв 2026

boost чем не устраивает?

#12
18:58, 19 янв 2026

v1c
> У нас в движке в одном месте есть path.concat("\\image.png") который просто не работает на Unix-платформе или при поиске в хешмапе если туда положили ресурс с другим separator convention.
Ну вот я и говорю, вставить проверку, чтобы на этом сразу падало.

#13
9:44, 20 янв 2026

v1c
Boost ?

#14
10:18, 20 янв 2026

innuendo

Давно у тебя голоса в голове? Лет 10 назад вроде бы был еще адекватным, нормально общаясь. Сейчас, куда ни загляни - "boost", "urho3d", "убогое апи", "атцы", "зачем тебе сабж", "абырвалг". Сиренькнет что-то на вентилятор одним словом, и сидит довольный.

+ и видимо испытывает фантомные боли, когда находится в бане
Страницы: 1 2 3 4 Следующая »
ПрограммированиеФорумОбщее