Конструктор массива небезопасных шаблонов
Это, наверное, простой вопрос, но у меня есть этот 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 вообще неподписанным? Могу ли я иметь пример, где он может иметь ошибку компиляции и/или времени выполнения?
Ответы
Ответ 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
> общую память в вашей системе.
Ответ 5
Небезопасно вызывать новый оператор внутри списка инициализации.
если новый сбой, деструктор массива не будет вызван.
вот аналогичный вопрос.
Есть ли проблемы с распределением памяти в списках инициализации конструктора?