Безопасная реализация strcpy с C ++

После чтения этой статьи я решил взять трещины при реализации функции «safe_strcpy», которая устраняет некоторые ошибки, о которых говорит автор. Очевидно, что моя функция safe_strcpy() вдохновлена ​​его. Ниже я создал простой класс, в котором содержится std::array одного из множества различных типов строк. Мои функции safe_strcpy() полагаются на всегда нулевое завершение заданного массива.

У меня есть несколько забот о моей реализации:

  1. Строки с out[N - 1] = static_cast<T>(0) кажутся неправильными, если базовый тип символа - wchar_t. В этом смысле я хочу, чтобы массив завершил что-то с эффектом L"0". Будет ли это работать? Если нет, как я могу его исправить?

  2. Должен ли я беспокоиться о любых проблемах с производительностью или о каких-либо «ошибках», о которых я не думал?

  3. Является ли это жизнеспособным решением для головоломки safe_strcpy? Другими словами, использовал бы you этот класс в производственном коде? Какие улучшения или расширения должны использовать этот класс?

#include <cstdlib>
#include <cstdio>
#include <cstring>
#include <array>
#include <iostream>
#include <typeinfo>
using namespace std;

template <size_t N, class T = char>
struct char_array
{
    typedef T                value_type;
    typedef const T          const_value_type;
    typedef T&               reference;
    typedef const T&         const_reference;
    typedef std::array<T, N> container;

    typedef typename container::size_type       size_type;
    typedef typename container::iterator        iterator;
    typedef typename container::const_iterator  const_iterator;

    typedef typename container::reverse_iterator        reverse_iterator;
    typedef typename container::const_reverse_iterator  const_reverse_iterator;

    /* Constructor */
    char_array()
    {
        static_assert((is_same<unsigned char, T>::value || is_same<char, T>::value ||
                      is_same<wchar_t, T>::value || is_same<signed char, T>::value ||
                      is_same<char16_t, T>::value || is_same<char32_t, T>::value)  &&
                      N > 0,
                      "char_array initialized with invalid type or size");
    }

    /* Destructor */
    ~char_array() {}

    /* Copy constructor and assignment */
    char_array(const char_array<N,T> &other) = default;
    char_array& operator=(const char_array<N,T> &other) = default;

    /* Move constructor and assignment */
    char_array(char_array<N,T> &&other) { buf_ = std::move(other.buf_); }
    char_array& operator=(char_array<N,T> &&other) 
    { 
        buf_ = std::move(other.buf_);
        return *this;
    } 

    /* Delegate members of internal container */

    iterator         begin() { return buf_.begin(); }
    const_iterator   begin() const { return buf_.begin(); }

    iterator         end() { return buf_.end(); }
    const_iterator   end() const { return buf_.end(); }

    const_iterator   cbegin() const { return buf_.cbegin(); }    
    const_iterator   cend() const { return buf_.cend(); }

    reverse_iterator         rbegin() { return buf_.rbegin(); }
    const_reverse_iterator   rbegin() const { return buf_.rbegin(); }

    reverse_iterator         rend() { return buf_.rend(); }
    const_reverse_iterator   rend() const { return buf_.rend(); }

    const_reverse_iterator   crbegin() const { return buf_.crbegin(); }
    const_reverse_iterator   crend() const { return buf_.crend(); }

    reference       operator[](size_type i) { return buf_[i]; }
    const_reference operator[](size_type i) const { return buf_[i]; }

    size_type        size() const { return buf_.size(); }
    size_type        max_size() const { return buf_.max_size(); }
    bool             empty() const { return buf_.empty(); }

    reference        at(size_type n) { return buf_.at(n); }
    const_reference  at(size_type n) const { return buf_.at(n); }

    reference        front(size_type n) { return buf_.front(n); }
    const_reference  front(size_type n) const { return buf_.front(n); }

    reference        back(size_type n) { return buf_.back(n); }
    const_reference  back(size_type n) const { return buf_.back(n); }

    value_type*       data() { return buf_.data(); }
    const value_type* data() const { return buf_.data(); }

    void fill(const value_type &val) { buf_.fill(val); }
    void swap(char_array &x) { buf_.swap(x.buf_); }

private:
    container buf_;
};

/*
    Allow "template magic" to automatically deduce
    the size of the given array. NOTE: this only works with
    statically-allocated arrays (e.g. arrays with a size known
    at compile time)
    @param[in] - out - a reference to the array
    @param[in] - src - the source string

    Usage:
    char buf[10];
    safe_strcpy(buf, "This is a string > 10 chars");
 */
template <class T, size_t N>
void safe_strcpy(T (&out)[N], const T *src)
{
    static_assert((is_same<wchar_t, T>::value || is_same<signed char, T>::value ||
                is_same<unsigned char, T>::value || is_same<char, T>::value || 
                is_same<char16_t, T>::value || is_same<char32_t, T>::value)  &&
                N > 0, "Incompatible type for safe_strcpy");
    memcpy(out, src, N * sizeof(T));
    out[N - 1] = static_cast<T>(0);
}

template <class T, size_t N, class S>
void safe_strcpy(char_array<N,T> &out, const S *src)
{
    static_assert(is_same<T, S>::value, "Source content type different from char_array destination");
    memcpy(out.data(), src, N * sizeof(T));
    out[N - 1] = static_cast<T>(0);
}

template <class Os, size_t N, class T>
Os& operator<<(Os &os, const char_array<N,T> &arr)
{
    for (const auto &c : arr) os << c;
    return os;
}

int main()
{
    wchar_t buf[10];
    safe_strcpy(buf, L"really long string that's greater than 10 freaking characters!!!!");
    wcout << buf << "\n";

    char_array<10> buffer;
    safe_strcpy(buffer, "This is a string longer than 10 chars");
    cout << "Max size is: " << buffer.max_size() << "\n";
    cout << buffer << "\n";
}
11 голосов | спросил Bizkit 22 TueEurope/Moscow2015-12-22T01:02:40+03:00Europe/Moscow12bEurope/MoscowTue, 22 Dec 2015 01:02:40 +0300 2015, 01:02:40

3 ответа


12

Разбить свой тип

У вас есть это большое количество is_same проверок в нескольких местах. Разбейте его по своему собственному признаку и назовите его что-то вроде is_char:

template <class T> is_char : std::false_type { };
template <> is_char<wchar_t> : std::true_type { };
template <> is_char<char> : std::true_type { };
template <> is_char<signed char> : std::true_type { };
...

Каким образом в вашем char_array вы можете просто:

static_assert(is_char<T>::value, "invalid type, expected a char");
static_assert(N > 0, "invalid size, expected N > 0");

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

Множество повторений кода

В значительной степени весь ваш код char_array просто дублирует то, что std::array. Что действительно дает вам char_array? Это стоит вам агрегатной инициализации, и это не совсем так. Ultimateyl, это действительно просто std::array<> с некоторыми дополнительными условиями на T и N.

Но мы можем использовать те же самые условия для SFINAE safe_strcpy:

template <class T, size_t N, class S,
          class = std::enable_if_t<is_char<T>::value &&
                                   (N > 0) &&
                                   is_same<T, S>::value>>
void safe_strcpy(std::array<T,N> &out, const S *src)
{
    memcpy(out.data(), src, N * sizeof(T));
    out[N - 1] = 0;
}

И теперь нам вообще не нужно использовать char_array.


Если вы действительно хотите сохранить тип, я бы просто наследовал:

template <size_t N, class T=char>
struct char_array : std::array<T, N>
{
    static_assert(is_char<T>::value, "invalid type, expected a char");
    static_assert(N > 0, "invalid size, expected N > 0");
};

memcpy для std::copy

Вместо копирования необработанных байтов и необходимости запоминания для умножения вы можете просто использовать ---- +: = 16 =: + ---- - что в любом случае будет делать одно и то же, но будет менее подверженным ошибкам. Поэтому вместо:

std::copy

Запись:

memcpy(out.data(), src, N * sizeof(T));
ответил Barry 22 TueEurope/Moscow2015-12-22T01:41:24+03:00Europe/Moscow12bEurope/MoscowTue, 22 Dec 2015 01:41:24 +0300 2015, 01:41:24
10
  • Гоча

    Существует вероятность незаконного доступа. Хотя код гарантирует, что буфер назначения не переполнен, он, тем не менее, будет иметь доступ к за пределами источника , если он меньше адресата. memcpy - это просто неправильный инструмент здесь.

  • Я буду использовать его?

    Учитывая очень ограниченный вариант использования этого подхода (только для статических буферов) и специализацию по размеру шаблона (серьезный потенциал для раздувания кода), я, вероятно, останусь с добрым старым strncpy. Слухи о его небезопасности сильно преувеличены.

ответил vnp 22 TueEurope/Moscow2015-12-22T01:38:03+03:00Europe/Moscow12bEurope/MoscowTue, 22 Dec 2015 01:38:03 +0300 2015, 01:38:03
3

Одна жесткая холодная правда заключается в том, что C-Strings не могут быть исправлены: незнание размера буфера (входящего или исходящего) является рецептом сбоя, а band-aids (strncpy, strlcpy, ...) просто пытаются исправить симптомы.

Еще одна жесткая холодная правда заключается в том, что wchar_t разбит по дизайну, поскольку он не переносится (16 бит на Windows и 32 бит на Unix). Поэтому вы не должны использовать его вообще и вместо этого полагаться на явно выраженные типы char16_t и char32_t


В результате ваш собственный код подвержен проблемам времени выполнения:

  • вы никогда не гарантируете, что src не является нулевым, по крайней мере, assert будет полезен, и документация должна его отразить, некоторые компиляторы также имеют конкретные аннотации (gcc __attribute__((nonnull(2))) например)
  • вы никогда не проверяете, что src достаточно длинный и вслепую скопируйте N, хотя, как правило, нет гарантии, что источник длиннее адресата

И имеет ряд ограничений дизайна:

  • вы не разрешаете копирование по частям (полезно для конкатенации ...)
  • вы разрешаете копирование только в статически выделенный буфер
  • вы обрабатываете строки как бессмысленную последовательность байтов и обрезаете без учета его кодировки (но затем, std::string делает это тоже ...)

Если вы не можете позволить себе std::string или хотите создать альтернативу, где владение буфером может быть внешним (чтобы использовать фиксированный размер буфер), то я советую использовать подход класса и просто сделать его совместимым с C-Strings для взаимодействия:

  • , разрешив построение из C-String
  • , оставив NUL-завершенным для дешевого преобразования в C-строку

И тогда этот класс может легко реализовать безопасную копию!

Последнее примечание: std::string может быть не таким дорогостоящим, как вы думаете, если вы избегаете его по причине производительности , сначала измерьте ...

Примерный пример (непроверенный!) более безопасной альтернативы C-String:

//
//  Fixed-Capacity String
//
//  It never allocates or re-allocates, but instead uses the
//  externally provided buffer.
//
template <typename T>
class fcstring {
public:
    //  Construction
    explicit fcstring(T* cstring):
        _capacity(cstring ? std::strlen(cstring) : 0),
        _size(_capacity),
        _buffer(cstring)
    {}

    template <size_t N>
    explicit fcstring(T (&str)[N]):
        _capacity(N-1),
        _size(N-1),
        _buffer(str)
    {}

    //  "buffer" is assumed to already point at a valid C-String
    //  of size "size" (not counting the terminating NUL-byte).
    //  "capacity" may be additionally provided if the buffer is known
    //  to be larger than "size", it then represents the number of "T"
    //  than the "buffer" may contain, not counting the NUL-byte.
    fcstring(T* buffer, size_t size, size_t capacity = 0):
        _capacity(capacity ? capacity : size),
        _size(size),
        _buffer(buffer)
    {}

    //  Copy
    void assign(fcstring const& other) {
        size_t const copied = std::min(_capacity, other._size);
        std::memcpy(_buffer, other._buffer, copied);
        _buffer[copied] = T();
        _size = copied;
    }

    //  Conversion to C-String
    //  /!\ Be mindful that any embedded NUL character will
    //  /!\ result in a truncated output
    T const* c_str() const { return _size ? _buffer : ""; }

private:
    size_t _capacity;
    size_t _size;
    T* _buffer;
}; // class fcstring
ответил Matthieu M. 22 TueEurope/Moscow2015-12-22T13:07:12+03:00Europe/Moscow12bEurope/MoscowTue, 22 Dec 2015 13:07:12 +0300 2015, 13:07:12

Похожие вопросы

Популярные теги

security × 330linux × 316macos × 2827 × 268performance × 244command-line × 241sql-server × 235joomla-3.x × 222java × 189c++ × 186windows × 180cisco × 168bash × 158c# × 142gmail × 139arduino-uno × 139javascript × 134ssh × 133seo × 132mysql × 132