Подтвердить что ты не робот

Конструктор массива небезопасных шаблонов

Это, наверное, простой вопрос, но у меня есть этот template class:

template<typename Type>
class Array {
    size_t n;
    Type* buff;
public:
    Array(size_t n_): n(n_), buff(new Type[n]) {}
};

Код из файла PDF курса, где он говорит, buff(new Type[n]) является небезопасным. Я не понимаю, почему это небезопасно, не является ли size_t вообще неподписанным? Могу ли я иметь пример, где он может иметь ошибку компиляции и/или времени выполнения?

4b9b3361

Ответ 1

Код "небезопасно" в том, что он полагается на n, который строится до buff. Эта зависимость добавляет хрупкость в код.

Когда вы создаете члены класса, они строятся в порядке, объявленном в классе, а не как они вызывают в списке инициализации члена, поэтому, если код был изменен на

template<typename Type>
class Array {
    Type* buff;
    size_t n;
public:
    Array(size_t n_): n(n_), buff(new Type[n]) {}
};

Затем, когда вы выполняете buff(new Type[n]), n не инициализируется, и у вас есть поведение undefined.

Ответ 2

Прежде всего порядок выполнения инициализации для конструктора не определяется порядком их записи, а порядком инициализированные поля появляются в коде:

class Array {
    size_t n;
    Type* buff;
public:
    Array(size_t n_): n(n_), buff(new Type[n]) {}
};

Здесь сначала будет инициализировано n, а затем buff.

class Array {
    Type* buff;
    size_t n;
public:
    Array(size_t n_): n(n_), buff(new Type[n]) {}
};

Теперь первый бафф будет инициализирован, а затем n, поэтому n не имеет определенного значения в этом случае.

Использование списков инициализации для конструкторов является хорошей практикой, но будьте осторожны, чтобы вы не создавали никаких допущений в порядке.

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

В конкретном случае вы также можете использовать std::vector вместо массива C-стиля. Он обрабатывает все распределения, перераспределения, выпуска и т.д. Безопасным потоком для вас. Похоже, вы пытаетесь написать что-то вроде своего std::vector. Пожалуйста, делайте это только в образовательных целях. Всегда предпочитайте стандартную реализацию в производственном коде. Скорее всего, вам это пока не удастся. Если бы вы это сделали, вы бы задали здесь разные вопросы: -)

Ответ 3

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

template<typename Type>
class Array {
    size_t n;
    Type* buff;
public:
    Array(size_t n_): n(n_), buff(new Type[n]) {}
    ~Array() { delete[] buff; }
};

Теперь этот код абсолютно безопасен. Никакое исключение не может быть выбрано при назначении n_, порядок инициализации верен, а buff - единственный необработанный указатель в вашем классе. Однако, когда вы начинаете расширять свой класс и записываете больше классов, риск утечки памяти увеличивается.

Представьте, что вам нужно добавить еще один элемент в class Array:

template<typename Type>
class Array {
    size_t n;
    Type* buff;
    SomethingElse xyz;
public:
    Array(size_t n_): n(n_), buff(new Type[n_]), xyz(n_) {}
    ~Array() { delete[] buff; }
};

Если конструктор SomethingElse выбрасывается, память, выделенная для buff, течет, потому что деструктор ~Array() никогда не будет вызываться.

Современные С++ вызовы, такие как Type* buff raw указатели, потому что вы несете ответственность за освобождение памяти самостоятельно (с учетом исключений) и вводите такие инструменты, как std::unique_ptr и std::shared_ptr, которые могут автоматически избавиться от освобождения памяти.

В современном С++ вы можете написать свой класс следующим образом:

template<typename Type>
class Array {
    size_t n;
    std::unique_ptr<Type[]> buff;
public:
    Array(size_t n_): n(n_), buff(new Type[n_]) {}
};

Обратите внимание на отсутствие деструктора. unique_ptr позаботится о вызове delete для вас.

Обратите также внимание на отсутствие зависимости от членов класса внутри списка инициализаторов (просто запись new Type[n_] вместо new Type[n] делает ваш код более надежным)

Ответ 4

С++ 98 Standard 12.6.2.5.4 (я не ожидаю, что новые версии смягчат это).

- Затем нестатические члены данных должны быть инициализированы в том порядке, в котором они были объявлены в определении класса (опять же независимо от порядка из mem-инициализаторов).

Таким образом, порядок инициализации определяется в соответствии с этим.

Если вам нужен пример того, как сбой, просто сделайте sizeof(Type)*n > общую память в вашей системе.