5. Распространенные подводные камни при использовании массивов.
5.1 Подводный камень: доверие к небезопасным ссылкам.
Хорошо, вам сказали или сами узнали, что глобальные переменные (переменные области имен пространства имен, к которым можно обращаться за пределами модуля перевода) - это Evil ™. Но знаете ли вы, насколько они злые ™? Рассмотрим программу ниже, состоящую из двух файлов [main.cpp] и [numbers.cpp]:
// [main.cpp]
#include <iostream>
extern int* numbers;
int main()
{
using namespace std;
for( int i = 0; i < 42; ++i )
{
cout << (i > 0? ", " : "") << numbers[i];
}
cout << endl;
}
// [numbers.cpp]
int numbers[42] = {1, 2, 3, 4, 5, 6, 7, 8, 9};
В Windows 7 это прекрасно компилируется и связывается как с MinGW g ++ 4.4.1, так и с Visual C ++ 10.0.
Поскольку типы не совпадают, при запуске программы происходит сбой.
Формальное объяснение: программа имеет неопределенное поведение (UB), и поэтому вместо сбоя она может просто зависнуть или, возможно, ничего не делать, или может послать угрожающие электронные письма президентам США, России, Индии, Китай и Швейцария, и заставить носовых демонов вылетать из носа.
Практическое объяснение: в main.cpp
массиве рассматривается как указатель, размещенный по тому же адресу, что и массив. Для 32-битного исполняемого файла это означает, что первое
int
значение в массиве рассматривается как указатель. Т.е., в переменный содержит или содержит , как представляется, . Это приводит к тому, что программа получает доступ к памяти внизу адресного пространства, которое традиционно резервируется и вызывает ловушку. Результат: вы получите сбой.main.cpp
numbers
(int*)1
Компиляторы полностью в пределах своих прав не диагностировать эту ошибку, потому что в C ++ 11 §3.5 / 10 говорится о требовании совместимых типов для объявлений,
[N3290 §3.5 / 10]
Нарушение этого правила для идентификации типа не требует диагностики.
В том же абзаце подробно описаны возможные варианты:
… Объявления для объекта массива могут указывать типы массивов, которые отличаются наличием или отсутствием привязки основного массива (8.3.4).
Это допустимое изменение не включает объявление имени в виде массива в одной единице перевода и в качестве указателя в другой единице перевода.
5.2 Ловушка: преждевременная оптимизация ( memset
и друзья).
Еще не написано
5.3 Подводный камень: Использование языка C для определения количества элементов.
С глубоким опытом C естественно написать…
#define N_ITEMS( array ) (sizeof( array )/sizeof( array[0] ))
Так как array
распадается указатель на первый элемент, где это необходимо, выражение sizeof(a)/sizeof(a[0])
также может быть записано как
sizeof(a)/sizeof(*a)
. Это означает то же самое, и независимо от того, как оно написано, это идиома C для поиска числовых элементов массива.
Основная ошибка: идиома небезопасна. Например, код ...
#include <stdio.h>
#define N_ITEMS( array ) (sizeof( array )/sizeof( *array ))
void display( int const a[7] )
{
int const n = N_ITEMS( a ); // Oops.
printf( "%d elements.\n", n );
}
int main()
{
int const moohaha[] = {1, 2, 3, 4, 5, 6, 7};
printf( "%d elements, calling display...\n", N_ITEMS( moohaha ) );
display( moohaha );
}
передает указатель на N_ITEMS
, и, следовательно, скорее всего, дает неправильный результат. Скомпилированный как 32-битный исполняемый файл в Windows 7, он производит ...
7 элементов, вызывающих дисплей ...
1 элемент.
- Компилятор переписывает
int const a[7]
просто int const a[]
.
- Компилятор переписывает
int const a[]
в int const* a
.
N_ITEMS
поэтому вызывается с указателем.
- Для 32-битного исполняемого файла
sizeof(array)
(размер указателя) тогда 4.
sizeof(*array)
эквивалентно sizeof(int)
, что для 32-разрядного исполняемого файла также 4.
Чтобы обнаружить эту ошибку во время выполнения, вы можете сделать ...
#include <assert.h>
#include <typeinfo>
#define N_ITEMS( array ) ( \
assert(( \
"N_ITEMS requires an actual array as argument", \
typeid( array ) != typeid( &*array ) \
)), \
sizeof( array )/sizeof( *array ) \
)
7 элементов, вызов дисплея ...
Утверждение не удалось: ("N_ITEMS требует фактического массива в качестве аргумента", typeid (a)! = Typeid (& * a)), файл runtime_detect ion.cpp, строка 16
Это приложение запросило Runtime прекратить его необычным способом.
Пожалуйста, обратитесь в службу поддержки приложения для получения дополнительной информации.
Обнаружение ошибок во время выполнения лучше, чем отсутствие обнаружения, но оно тратит немного процессорного времени и, возможно, намного больше программистского времени. Лучше с обнаружением во время компиляции! И если вы счастливы не поддерживать массивы локальных типов с C ++ 98, то вы можете сделать это:
#include <stddef.h>
typedef ptrdiff_t Size;
template< class Type, Size n >
Size n_items( Type (&)[n] ) { return n; }
#define N_ITEMS( array ) n_items( array )
Скомпилировав это определение, подставленное в первую полную программу, с g ++, я получил…
M: \ count> g ++ compile_time_detection.cpp
compile_time_detection.cpp: в функции 'void display (const int *)':
compile_time_detection.cpp: 14: ошибка: нет соответствующей функции для вызова 'n_items (const int * &)'
M: \ count> _
Как это работает: массив передается по ссылке наn_items
, и поэтому он не гниет с указателем на первый элемент, а функция просто возвращает количество элементов , указанных типа.
С C ++ 11 вы можете использовать это также для массивов локального типа, и это
типизированная идиома C ++ для нахождения количества элементов массива.
5.4 Подводные камни C ++ 11 и C ++ 14: Использование constexpr
функции размера массива.
С C ++ 11 и более поздними версиями это естественно, но, как вы увидите, опасно !, заменить функцию C ++ 03
typedef ptrdiff_t Size;
template< class Type, Size n >
Size n_items( Type (&)[n] ) { return n; }
с
using Size = ptrdiff_t;
template< class Type, Size n >
constexpr auto n_items( Type (&)[n] ) -> Size { return n; }
где существенным изменением является использование constexpr
, которое позволяет этой функции создавать постоянную времени компиляции .
Например, в отличие от функции C ++ 03, такая константа времени компиляции может использоваться для объявления массива того же размера, что и другой:
// Example 1
void foo()
{
int const x[] = {3, 1, 4, 1, 5, 9, 2, 6, 5, 4};
constexpr Size n = n_items( x );
int y[n] = {};
// Using y here.
}
Но рассмотрим этот код, используя constexpr
версию:
// Example 2
template< class Collection >
void foo( Collection const& c )
{
constexpr int n = n_items( c ); // Not in C++14!
// Use c here
}
auto main() -> int
{
int x[42];
foo( x );
}
Подводный камень: по состоянию на июль 2015 года вышеперечисленное компилируется с MinGW-64 5.1.0 с
-pedantic-errors
, и, тестируя с онлайн-компиляторами на gcc.godbolt.org/ , также с clang 3.0 и clang 3.2, но не с clang 3.3, 3.4. 1, 3,5,0, 3,5,1, 3,6 (rc1) или 3,7 (экспериментально). И что важно для платформы Windows, она не компилируется с Visual C ++ 2015. Причина в том, что в C ++ 11 / C ++ 14 говорится об использовании ссылок в constexpr
выражениях:
C ++ 11 C ++ 14 $ 5,19 / 2 девять
го тира
Условное выражение e
является выражением постоянная сердечника , если только оценки e
, следуя правила абстрактной машины (1.9), будет оценивать одно из следующих выражений:
⋮
- ID-выражение , которое относится к элементу или переменных данных ссылочного типа , если ссылка не имеет предшествующую инициализацию и либо
- он инициализируется постоянным выражением или
- это нестатический элемент данных объекта, время жизни которого началось в пределах оценки e;
Всегда можно написать более многословный
// Example 3 -- limited
using Size = ptrdiff_t;
template< class Collection >
void foo( Collection const& c )
{
constexpr Size n = std::extent< decltype( c ) >::value;
// Use c here
}
... но это не удается, когда Collection
не является необработанным массивом.
Чтобы иметь дело с коллекциями, которые могут быть не массивами, требуется перегрузка
n_items
функции, но также для использования во время компиляции необходимо представление размера массива во время компиляции. И классическое решение C ++ 03, которое отлично работает также в C ++ 11 и C ++ 14, состоит в том, чтобы позволить функции сообщать о своем результате не как значение, а через свой тип результата функции . Например, вот так:
// Example 4 - OK (not ideal, but portable and safe)
#include <array>
#include <stddef.h>
using Size = ptrdiff_t;
template< Size n >
struct Size_carrier
{
char sizer[n];
};
template< class Type, Size n >
auto static_n_items( Type (&)[n] )
-> Size_carrier<n>;
// No implementation, is used only at compile time.
template< class Type, size_t n > // size_t for g++
auto static_n_items( std::array<Type, n> const& )
-> Size_carrier<n>;
// No implementation, is used only at compile time.
#define STATIC_N_ITEMS( c ) \
static_cast<Size>( sizeof( static_n_items( c ).sizer ) )
template< class Collection >
void foo( Collection const& c )
{
constexpr Size n = STATIC_N_ITEMS( c );
// Use c here
(void) c;
}
auto main() -> int
{
int x[42];
std::array<int, 43> y;
foo( x );
foo( y );
}
О выборе типа возвращаемого значения для static_n_items
: этот код не используется, std::integral_constant
поскольку std::integral_constant
результат представляется непосредственно в виде constexpr
значения, вновь возвращая исходную проблему. Вместо Size_carrier
класса можно позволить функции напрямую возвращать ссылку на массив. Однако не все знакомы с этим синтаксисом.
О наименовании: часть этого решения проблемы constexpr
-invalid -по-ссылке-сделать явный выбор постоянной времени компиляции.
Надеемся, что проблема «упс, была ссылка вовлечена в вашу constexpr
проблему» будет исправлена в C ++ 17, но до этого макрос, подобный приведенному STATIC_N_ITEMS
выше, дает переносимость, например, компиляторам clang и Visual C ++, сохраняя тип безопасность.
Связанный: макросы не относятся к областям видимости, поэтому, чтобы избежать конфликтов имен, было бы неплохо использовать префикс имени, например MYLIB_STATIC_N_ITEMS
.