Про безопасный C++
От: Shmj Ниоткуда  
Дата: 21.09.24 04:39
Оценка: :)))
На мысль навела статья.

Вот в C++ принято что нет проверки выхода за пределы массива. Т.е. обратились по индексу 6, в то время как элементов всего 6 — вам даже не сообщат об ошибке. Считается что C++-разработчики не могут допустить таких ошибок и если ты допустил — то вон из профессии, таким как ты тут не место (ну или хотя бы никому не говори об этом).

Добавлю оговорку. Есть at(), который как бы на отвяжись добавили. Однако же он не решает проблему, т.к. даже при банальном копировании данных он вызываться не будет (см. пример ниже).

Это можно оправдать скоростью работы, ведь каждая проверка требует доп. инструкций процессора. Было бы неплохо иметь возможность хотя бы применять такие проверки по некой опции, к примеру добавить флаг сборки или что-то подобное. Когда уже отладил — флаг можно и убрать, в принципе. Более того — скорость не всегда критична и не всегда на первом месте — иногда важнее точность работы кода и быстрое обнаружение ошибок.

И вопрос такой. В принципе то C++ не виноват, он же не запрещает проверки выхода за пределы. Однако же виновата философия, которая говорит что такие проверки нужны только школьникам, что настоящие программисты не ошибаются. И я подумал грешным делом — а что если просто добавить обертки для стандартных классов, которые выполняют такие проверки? Не слишком ли смелое решение?

Как оказалось, дело это не такое уж тривиальное. Вот, к примеру, GPT выдал для проверки c std::ranges — но это не всегда работает как нужно:

  Скрытый текст
#include <iostream>
#include <stdexcept>
#include <ranges>
#include <algorithm>
#include <iterator>
#include <concepts>
#include <vector>

template<typename T>
class SafeArray
{
public:
    using value_type = T;
    using difference_type = std::ptrdiff_t;

    class Iterator
    {
    public:
        using iterator_concept = std::random_access_iterator_tag;
        using iterator_category = std::random_access_iterator_tag;
        using value_type = T;
        using difference_type = std::ptrdiff_t;
        using pointer = T*;
        using reference = T&;

        Iterator() : ptr_(nullptr), begin_(nullptr), end_(nullptr) {}
        Iterator(T* ptr, T* begin, T* end) : ptr_(ptr), begin_(begin), end_(end) {}

        // Dereference with bounds checking
        reference operator*() const
        {
            if (ptr_ < begin_ || ptr_ >= end_)
            {
                throw std::out_of_range("Iterator out of bounds on dereference");
            }
            return *ptr_;
        }

        pointer operator->() const
        {
            return &**this;
        }

        // Increment and decrement with bounds checking
        Iterator& operator++()
        {
            if (ptr_ + 1 > end_)
            {
                throw std::out_of_range("Iterator goes beyond array bounds on increment");
            }
            ++ptr_;
            return *this;
        }

        Iterator operator++(int)
        {
            Iterator tmp = *this;
            ++(*this);
            return tmp;
        }

        Iterator& operator--()
        {
            if (ptr_ - 1 < begin_)
            {
                throw std::out_of_range("Iterator goes beyond array bounds on decrement");
            }
            --ptr_;
            return *this;
        }

        Iterator operator--(int)
        {
            Iterator tmp = *this;
            --(*this);
            return tmp;
        }

        // Arithmetic operations with bounds checking
        Iterator operator+(difference_type n) const
        {
            T* new_ptr = ptr_ + n;
            if (new_ptr < begin_ || new_ptr > end_)
            {
                throw std::out_of_range("Iterator goes beyond array bounds on addition");
            }
            return Iterator(new_ptr, begin_, end_);
        }

        Iterator operator-(difference_type n) const
        {
            T* new_ptr = ptr_ - n;
            if (new_ptr < begin_ || new_ptr > end_)
            {
                throw std::out_of_range("Iterator goes beyond array bounds on subtraction");
            }
            return Iterator(new_ptr, begin_, end_);
        }

        difference_type operator-(const Iterator& other) const
        {
            return ptr_ - other.ptr_;
        }

        Iterator& operator+=(difference_type n)
        {
            *this = *this + n;
            return *this;
        }

        Iterator& operator-=(difference_type n)
        {
            *this = *this - n;
            return *this;
        }

        reference operator[](difference_type n) const
        {
            return *(*this + n);
        }

        // Comparison operators
        bool operator==(const Iterator& other) const
        {
            return ptr_ == other.ptr_;
        }

        bool operator!=(const Iterator& other) const
        {
            return ptr_ != other.ptr_;
        }

        bool operator<(const Iterator& other) const
        {
            return ptr_ < other.ptr_;
        }

        bool operator>(const Iterator& other) const
        {
            return ptr_ > other.ptr_;
        }

        bool operator<=(const Iterator& other) const
        {
            return ptr_ <= other.ptr_;
        }

        bool operator>=(const Iterator& other) const
        {
            return ptr_ >= other.ptr_;
        }

    private:
        T* ptr_;
        T* begin_;
        T* end_;
    };

    using iterator = Iterator;
    using const_iterator = Iterator;

    SafeArray(std::size_t size) : size_(size), data_(new T[size]) {}

    ~SafeArray()
    {
        delete[] data_;
    }

    T& operator[](std::size_t index)
    {
        if (index >= size_)
        {
            throw std::out_of_range("Index out of bounds");
        }
        return data_[index];
    }

    const T& operator[](std::size_t index) const
    {
        if (index >= size_)
        {
            throw std::out_of_range("Index out of bounds");
        }
        return data_[index];
    }

    iterator begin()
    {
        return Iterator(data_, data_, data_ + size_);
    }

    iterator end()
    {
        return Iterator(data_ + size_, data_, data_ + size_);
    }

    const_iterator begin() const
    {
        return Iterator(data_, data_, data_ + size_);
    }

    const_iterator end() const
    {
        return Iterator(data_ + size_, data_, data_ + size_);
    }

    std::size_t size() const
    {
        return size_;
    }

private:
    std::size_t size_;
    T* data_;
};

int main()
{
    SafeArray<int> safeArray(5);

    // Initialize the array
    for (std::size_t i = 0; i < safeArray.size(); ++i)
    {
        safeArray[i] = static_cast<int>(i + 1);
    }

    std::vector<int> output(5, 0);  // Vector to copy data

    try
    {
        // Attempt to copy all elements from safeArray to output
        std::ranges::copy(safeArray.begin(), safeArray.end(), output.begin());

        // Display copied data
        for (const auto& val : output)
        {
            std::cout << val << " ";
        }
        std::cout << std::endl;

        // Attempt to copy 8 elements, which goes beyond array bounds
        std::ranges::copy(safeArray.begin(), safeArray.end() + 1, output.begin());
    }
    catch (const std::out_of_range& e)
    {
        std::cerr << "Caught exception: " << e.what() << std::endl;
    }

    return 0;
}


Ну и сам вопрос. Может кто-то уже думал в этом направлении и есть такие безопасные обертки?
Отредактировано 21.09.2024 6:24 Shmj . Предыдущая версия . Еще …
Отредактировано 21.09.2024 6:23 Shmj . Предыдущая версия .
Re: Про безопасный C++
От: Muxa  
Дата: 21.09.24 06:09
Оценка:
S>Ну и сам вопрос. Может кто-то уже думал в этом направлении и есть такие безопасные обертки?

Да, кто-то подумал: https://en.cppreference.com/w/cpp/container/vector/at
Re[2]: Про безопасный C++
От: Shmj Ниоткуда  
Дата: 21.09.24 06:20
Оценка:
Здравствуйте, Muxa, Вы писали:

M>Да, кто-то подумал: https://en.cppreference.com/w/cpp/container/vector/at


Посмотрите мой пример — это никак не повлияет на использование стандартных алгоритмов, к примеру деже если просто скопируете и укажете индекс более чем доступно.
Re: Про безопасный C++
От: ononim  
Дата: 21.09.24 06:46
Оценка:
Если у тебя GCC то включи макрос _GLIBCXX_DEBUG — https://gcc.gnu.org/onlinedocs/libstdc++/manual/debug_mode_using.html#debug_mode.using.mode
У микрософта тоже есть такая штука: https://learn.microsoft.com/en-us/cpp/standard-library/checked-iterators?view=msvc-170
Как много веселых ребят, и все делают велосипед...
Re[2]: Про безопасный C++
От: Shmj Ниоткуда  
Дата: 21.09.24 07:05
Оценка:
Здравствуйте, ononim, Вы писали:

O>Если у тебя GCC то включи макрос _GLIBCXX_DEBUG — https://gcc.gnu.org/onlinedocs/libstdc++/manual/debug_mode_using.html#debug_mode.using.mode

O>У микрософта тоже есть такая штука: https://learn.microsoft.com/en-us/cpp/standard-library/checked-iterators?view=msvc-170

А если clang?
Re: Про безопасный C++
От: kov_serg Россия  
Дата: 21.09.24 07:11
Оценка:
Здравствуйте , Shmj, Вы писали:

S>На мысль навела статья.


S>Ну и сам вопрос. Может кто-то уже думал в этом направлении и есть такие безопасные обертки?

Тысячи их. Python например
Большая часть кода не является критически важной по скорости и может выполняться на языках которые не так быстры, но зато без UB, требуют меньше писанины, проще, удобней и дешевле. А вот те части которые требуют интенсивных вычислений можете хоть на фортране писать или использовать готовые оптимизированные библиотеки для типовых вычислительных задач. Для повышения ЧСВ быстрых вставок можно terra пощупать.
Re[3]: Про безопасный C++
От: ononim  
Дата: 21.09.24 07:20
Оценка: 4 (1)
O>>Если у тебя GCC то включи макрос _GLIBCXX_DEBUG — https://gcc.gnu.org/onlinedocs/libstdc++/manual/debug_mode_using.html#debug_mode.using.mode
O>>У микрософта тоже есть такая штука: https://learn.microsoft.com/en-us/cpp/standard-library/checked-iterators?view=msvc-170
S>А если clang?
_LIBCPP_DEBUG — https://releases.llvm.org/12.0.0/projects/libcxx/docs/DesignDocs/DebugMode.html
А еще там есть санитайзеры https://github.com/google/sanitizers
Как много веселых ребят, и все делают велосипед...
Отредактировано 21.09.2024 7:28 ononim . Предыдущая версия .
Re[2]: Про безопасный C++
От: Shmj Ниоткуда  
Дата: 21.09.24 07:22
Оценка:
Здравствуйте, kov_serg, Вы писали:

_>Тысячи их. Python например


Ну в Rust по умолчанию есть проверки и наоборот, чтобы сделать что-то без проверок — нужно вызывать спец. версии методов. При этом он не слишком медленнее C++ как-то.

Возможно и в C++ можно добавить такие проверки, хотя бы опционально на этапе отладки?
Re[4]: Про безопасный C++
От: Shmj Ниоткуда  
Дата: 21.09.24 07:27
Оценка:
Здравствуйте, ononim, Вы писали:

O>_LIBCPP_DEBUG — https://releases.llvm.org/12.0.0/projects/libcxx/docs/DesignDocs/DebugMode.html


Почему не работает:

#define _LIBCPP_DEBUG 1

#include <vector>
#include <ranges>
#include <iostream>

int main()
{
    std::vector<int> vector1 = { 1,2,3,4,5 };
    std::vector<int> vector2(5);
    
    try
    {
        std::ranges::copy(vector1 | std::views::drop(11) | std::views::take(100), vector2.begin() + 1);
    }
    catch (const std::out_of_range& e)
    {
        std::cerr << "Caught exception: " << e.what() << std::endl;
    }
    
    for (const auto& val : vector2)
    {
        std::cout << val << " ";
    }
    std::cout << std::endl;

    return 0;
}


?
Re: Про безопасный C++
От: vsb Казахстан  
Дата: 21.09.24 07:32
Оценка:
На мой взгляд проблема несколько глубже, чем просто добавить проверки.

Действительно проверки будут генерировать медленный код. Но, очевидно, во многих случаях компилятор может эти проверки убрать, если докажет, что они избыточны.

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

Я не знаю, насколько создатели Rust пытались следовать этому подходу, да и в принципе не знаю, насколько это возможно, но по факту в Rust в безопасном коде проверки есть, и в то же время при использовании функционального стиля компилятору хватает информации, чтобы часто генерировать код без проверок.

Очевидно, что в С++ этот подход уже не сработает, стандартная библиотека такая, какая есть и компиляторы такие, какие есть.

Ещё интереснен подход в Pascal. Там, насколько я помню, генерацию проверок можно было отключать для конкретных участков кода. Это тоже хороший подход, но в C++ кажется так не получится сделать.
Re[5]: Про безопасный C++
От: ononim  
Дата: 21.09.24 07:43
Оценка:
S>Почему не работает:
S> std::vector<int> vector1 = { 1,2,3,4,5 };
S> std::vector<int> vector2(5);
S> std::ranges::copy(vector1 | std::views::drop(11) | std::views::take(100), vector2.begin() + 1);
например потому что copy тут ничего не копирует, не? убери drop(11)
Как много веселых ребят, и все делают велосипед...
Re[6]: Про безопасный C++
От: Shmj Ниоткуда  
Дата: 21.09.24 08:15
Оценка:
Здравствуйте, ononim, Вы писали:

O>например потому что copy тут ничего не копирует, не? убери drop(11)


Так должно же показать что вышли за пределы по идее?
Re: Про безопасный C++
От: LaptevVV Россия  
Дата: 21.09.24 08:17
Оценка: +2
S>И вопрос такой. В принципе то C++ не виноват, он же не запрещает проверки выхода за пределы. Однако же виновата философия, которая говорит что такие проверки нужны только школьникам, что настоящие программисты не ошибаются. И я подумал грешным делом — а что если просто добавить обертки для стандартных классов, которые выполняют такие проверки? Не слишком ли смелое решение?
Дело не в школьниках.
дело в эффективности
Проверка индекса при каждом обращении к массиву — офигенно затратное дело...
Хочешь быть счастливым — будь им!
Без булдырабыз!!!
Re[7]: Про безопасный C++
От: ononim  
Дата: 21.09.24 09:14
Оценка:
O>>например потому что copy тут ничего не копирует, не? убери drop(11)
S>Так должно же показать что вышли за пределы по идее?
если ты думаешь что std::drop выходит за пределы, то нет
Как много веселых ребят, и все делают велосипед...
Re: Про безопасный C++
От: velkin Удмуртия https://kisa.biz
Дата: 21.09.24 10:19
Оценка: -1
Здравствуйте, Shmj, Вы писали:

S>На мысль навела статья.


В C++ сохранилось множество старинных возможностей, которыми, как правило, следует пренебрегать в пользу эквивалентных фич из «современного С++», если только нет явно причины поступить наоборот (char *, массивы, указатели, malloc/free, NULL, т.д.)


Как-то автор странно обзывает Си.

S>Вот в C++ принято что нет проверки выхода за пределы массива. Т.е. обратились по индексу 6, в то время как элементов всего 6 — вам даже не сообщат об ошибке.


Почитай книгу STL для программистов на C++. Аммерааль Леен. Может быть всё совсем не так плохо как тебе кажется.

S>Как оказалось, дело это не такое уж тривиальное. Вот, к примеру, GPT выдал для проверки c std::ranges — но это не всегда работает как нужно:


Думаю людям нужно читать книги конкретно по алгоритмам, а не пытаться кидать исключения в предполагаемых битомолках. Выход за границу массива это не ошибка языка программирования, это ошибка или не ошибка программиста.

Может быть программист хотел выйти за границу массива, так что ему запрещать теперь. Или пользуйся итератором, тот не позволит выйти за границу массива, если это указать. Да хоть размером массива какая разница.

S>И я подумал грешным делом — а что если просто добавить обертки для стандартных классов, которые выполняют такие проверки? Не слишком ли смелое решение?


Если тебе это так надо, то можешь сделать в режиме отладки, а в релизе использовать нормальную версию. Такую проверку делают макросами в коде, а для замены типа можно использовать typedef.

А вообще, если ты параноик и сам себе не доверяешь, то есть TDD (Разработка через тестирование). Проверяй всё, что делаешь до того, как делаешь.

S>Считается что C++-разработчики не могут допустить таких ошибок и если ты допустил — то вон из профессии, таким как ты тут не место (ну или хотя бы никому не говори об этом).


Просто исправь ошибку и всё.
Re[2]: Про безопасный C++
От: Jack128  
Дата: 21.09.24 11:23
Оценка:
Здравствуйте, vsb, Вы писали:

vsb>Ещё интереснен подход в Pascal. Там, насколько я помню, генерацию проверок можно было отключать для конкретных участков кода. Это тоже хороший подход, но в C++ кажется так не получится сделать.


Подход интересный, но на практике он не используется. Чистыми массивами пользуются не так то часто, а вот в аналоге vector (TList/TList<T>) эта возможность игнорируется, используется подход плюсов, для кода с проверками и кода без проверок требуется писать разный код
Re: Про безопасный C++
От: Великий Реверс google
Дата: 21.09.24 11:36
Оценка: :)
https://www.open-std.org/jtc1/sc22/wg21/docs/papers/2024/p3390r0.html
иди присоединяйся к дядькам

а вообще создай тему "почему земля круглая" в науч-попе
и делай свое грязное дело, там
нечего православный раздел загрязнять своей ересью
Re[2]: Про безопасный C++
От: Евгений Музыченко Франция https://software.muzychenko.net/ru
Дата: 21.09.24 12:44
Оценка: +1
Здравствуйте, vsb, Вы писали:

vsb>проверки будут генерировать медленный код


Не "медленный", а "чуть более медленный". Для подавляющего большинства программ разница не будет заметна без измерений. Но управляться такие проверки должны обязательно.

vsb>компилятор может эти проверки убрать, если докажет, что они избыточны.


Или когда программист явно укажет, что они не нужны. Тогда сразу будет видно — то ли программист достаточо умный (попросил отключить проверки для критичного по времени/объему кода), то ли самонадеянный дурак (попросил отключить глобально, даже в отладочных сборках).

vsb>чтобы: в стандартной библиотеке были проверки


Обращения ко встроенным массивам не имеют никакого отношения к стандартной библиотеке. А библиотеки коммерческих компиляторов как раз имеют встроенные проверки.

, но в то же время типовой код позволял бы компилятору выбрасывать эти проверки, не жертвуя корректностью.

vsb>Очевидно, что в С++ этот подход уже не сработает, стандартная библиотека такая, какая есть и компиляторы такие, какие есть.


Компилияторы постоянно дорабатываются. При этом одни умели вставлять проверки еще в прошлом веке, а другие до сих пор не умеют.

vsb>Ещё интереснен подход в Pascal. Там, насколько я помню, генерацию проверок можно было отключать для конкретных участков кода.


Это возможно в любом языке, в C/C++ для этого используются pragmas. Но вот уровень реализации фантастически, до позора, убог.

vsb>в C++ кажется так не получится сделать.


Это, как раз, одна из наиболее простых в реализации, и одновременно эффективных вещей, которую хорошо бы иметь любому компилятору.
Re[2]: Про безопасный C++
От: Евгений Музыченко Франция https://software.muzychenko.net/ru
Дата: 21.09.24 12:45
Оценка:
Здравствуйте, LaptevVV, Вы писали:

LVV>Проверка индекса при каждом обращении к массиву — офигенно затратное дело...


Да и хрен бы с ним. Их с самого начала надо было вставлять по умолчанию, и нехай отключают явно, лишь когда выяснилось, что они реально тормозят.
Re[6]: Про безопасный C++
От: Shmj Ниоткуда  
Дата: 21.09.24 13:09
Оценка:
Здравствуйте, ononim, Вы писали:

O>например потому что copy тут ничего не копирует, не? убери drop(11)


Ну вот так копирует мусор, компилятор никак не предупреждает и не бьет по рукам:

#define _LIBCPP_DEBUG 1

#include <vector>
#include <ranges>
#include <iostream>

int main()
{
    std::vector<int> vector1 = { 1,2 };
    std::vector<int> vector2= { 1,2,3,4,5 };
    
    try
    {
        std::ranges::copy(vector1.begin(), vector1.end() + 3, vector2.begin());
    }
    catch (const std::out_of_range& e)
    {
        std::cerr << "Caught exception: " << e.what() << std::endl;
    }
    
    for (const auto& val : vector2)
    {
        std::cout << val << " ";
    }
    std::cout << std::endl;

    return 0;
}
Подождите ...
Wait...
Пока на собственное сообщение не было ответов, его можно удалить.