5. Распространенные подводные камни при использовании массивов.
5.1 Подводный камень: доверие к небезопасным ссылкам.
ОК, вам сказали или сами узнали, что глобальные переменные (пространство имен
переменные области видимости, которые могут быть доступны за пределами модуля перевода)
Зло & торговли ;. Но знаете ли вы, как истинно зло и торговля; они есть? Рассмотрим
нижеприведенная программа, состоящая из двух файлов [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]
Нарушение этого правила в отношении идентификации типа не требует диагностики.
В этом же абзаце подробно описан допустимый вариант:
* * & Одна тысяча тридцать восемь hellip; объявления для объекта массива могут указывать типы массивов, которые
отличаются наличием или отсутствием привязки основного массива (8.3.4).
Это разрешенное изменение не включает объявление имени в виде массива в одном
блок перевода, а так же указатель в другой блок перевода.
5.2 Подводный камень: преждевременная оптимизация (memset
и друзья).
Еще не написано
5.3 Ловушка: использование языка C для определения количества элементов.
С глубоким опытом C вполне естественно писать & hellip;
#define N_ITEMS( array ) (sizeof( array )/sizeof( array[0] ))
Поскольку array
распадается на указатель на первый элемент, где это необходимо,
Выражение sizeof(a)/sizeof(a[0])
также можно записать в виде
sizeof(a)/sizeof(*a)
. Это означает то же самое, и независимо от того, как это
записано это C идиома для поиска числовых элементов массива.
Основная ошибка: идиома небезопасна. Например, код
& Hellip;
#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, он создает & hellip;
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.
Чтобы обнаружить эту ошибку во время выполнения, вы можете сделать & hellip;
#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
Это приложение запросило среду выполнения прекратить его необычным способом.
Пожалуйста, обратитесь в службу поддержки приложения для получения дополнительной информации.
Обнаружение ошибок во время выполнения лучше, чем отсутствие обнаружения, но затрачивается немного
время процессора и, возможно, гораздо больше времени программиста. Лучше с обнаружением в
КолорадоВремя миль! И если вы счастливы не поддерживать массивы локальных типов с 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 ++,
Я получил & hellip;
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
и лязг 3.2, но не с лязг 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 девять
й тире
A условное выражение e
является основным константным выражением , если только оценка
e
, следуя правилам абстрактной машины (1.9), оценил бы один из
следующие выражения:
11
- an id-выражение , которое относится к переменной или элементу данных ссылочного типа
если ссылка не имеет предшествующей инициализации или
- инициализируется константным выражением или
- это нестатический элемент данных объекта, время жизни которого началось в
оценка е;
Всегда можно написать более многословный
// 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
}
& hellip; но это терпит неудачу, когда 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
-неправильно-из-за-ссылки
проблема заключается в том, чтобы сделать выбор постоянной времени компиляции явным.
Надеюсь, проблема "упс-там-была-ссылка-вовлечена-в-твоем" constexpr
будет исправлена
C ++ 17, но до тех пор макрос, такой как STATIC_N_ITEMS
выше, дает переносимость,
например компиляторам Clang и Visual C ++, сохраняя безопасность типов.
Связанный: макросы не учитывают области видимости, поэтому во избежание конфликтов имен это может быть
идтиИдея использовать префикс имени, например MYLIB_STATIC_N_ITEMS
.