То, что нельзя проверить во время компиляции, должно быть проверяемым во время выполнения
Причина
Оставлять трудно обнаруживаемые ошибки в программе — значит напрашиваться на сбои и неверные результаты.
Примечание
В идеале мы обнаруживаем все ошибки (которые не являются ошибками в логике программиста) либо во время компиляции, либо во время выполнения. Невозможно обнаружить все ошибки во время компиляции, и зачастую недостаточно ресурсов для обнаружения всех оставшихся ошибок во время выполнения. Тем не менее мы должны стремиться писать программы, которые в принципе могут быть проверены при наличии достаточных ресурсов (программ анализа, проверок времени выполнения, аппаратных ресурсов, времени).
Пример (плохой)
// скомпилировано отдельно, возможно, динамически загружено
extern void f(int* p);
void g(int n)
{
// плохо: количество элементов не передаётся в f()
f(new int[n]);
}
Здесь важная информация (количество элементов) была настолько тщательно «скрыта», что статический анализ, вероятно, стал невозможным, а динамическая проверка может быть очень затруднена, когда f() является частью ABI, так что мы не можем «инструментировать» этот указатель. Мы могли бы встроить полезную информацию в свободное хранилище, но это потребует глобальных изменений в системе, а возможно, и в компиляторе. Перед нами проект, делающий обнаружение ошибок крайне затруднённым.
Пример (плохой)
Конечно, мы можем передать количество элементов вместе с указателем:
// скомпилировано отдельно, возможно, динамически загружено
extern void f2(int* p, int n);
void g2(int n)
{
// плохо: неправильное количество элементов может быть передано в f2()
f2(new int[n], n);
}
Передача количества элементов в качестве аргумента лучше (и значительно распространённее), чем просто передача указателя с опорой на какое-то (неуказанное) соглашение о знании или обнаружении количества элементов. Однако (как показано), простая опечатка может привести к серьёзной ошибке. Связь между двумя аргументами f2() является соглашением, а не явной.
Кроме того, неявно предполагается, что f2() должна delete свой аргумент (или вызывающая сторона допустила вторую ошибку?).
Пример (плохой)
Умные указатели для управления ресурсами стандартной библиотеки не передают размер, когда указывают на объект:
// скомпилировано отдельно, возможно, динамически загружено
// NB: это предполагает, что вызывающий код совместим с ABI, использует
// совместимый компилятор C++ и ту же реализацию stdlib
extern void f3(unique_ptr<int[]>, int n);
void g3(int n)
{
f3(make_unique<int[]>(n), m); // плохо: передача владения и размера по отдельности
}
Пример
Нам нужно передать указатель и количество элементов как единый объект:
extern void f4(vector<int>&); // скомпилировано отдельно, возможно, динамически загружено
extern void f4(span<int>); // скомпилировано отдельно, возможно, динамически загружено
// NB: это предполагает, что вызывающий код совместим с ABI, использует
// совместимый компилятор C++ и ту же реализацию stdlib
void g3(int n)
{
vector<int> v(n);
f4(v); // передать ссылку, сохранить владение
f4(span<int>{v}); // передать представление, сохранить владение
}
Этот дизайн несёт количество элементов как неотъемлемую часть объекта, так что ошибки маловероятны и динамическая (во время выполнения) проверка всегда возможна, если не всегда оправдана.
Пример
Как передать и владение, и всю информацию, необходимую для проверки корректности использования?
vector<int> f5(int n) // OK: перемещение
{
vector<int> v(n);
// ... инициализируем v ...
return v;
}
unique_ptr<int[]> f6(int n) // плохо: теряется n
{
auto p = make_unique<int[]>(n);
// ... инициализируем *p ...
return p;
}
owner<int*> f7(int n) // плохо: теряется n и можно забыть вызвать delete
{
owner<int*> p = new int[n];
// ... инициализируем *p ...
return p;
}
Контроль
- Помечать интерфейсы в стиле (указатель, количество) (это помечает много примеров, которые нельзя исправить из соображений совместимости)
- ???