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};
In Windows 7 this compiles and links fine with both MinGW g++ 4.4.1 and
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 для поиска числовых элементов массива.
Основная ошибка: идиома 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 элементов, вызов display ...
Ошибка утверждения: («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 ++, я получил
M: \ count> g ++ compile_time_detection.cpp
compile_time_detection.cpp: В функции 'void display (const int *)':
compile_time_detection.cpp: 14: error: нет соответствующей функции для вызова '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 nine
th dash
условное-выражение e
является основным константным выражением, если только оценка e
, следуя правилам абстрактной машины (1.9), не оценила бы одно из следующих выражений: < br> ⋮
- an id-expression that refers to a variable or data member of reference type
unless the reference has a preceding initialization and either
- it is initialized with a constant expression or
- это нестатический член данных объекта, время существования которого началось в пределах вычисления 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, состоит в том, чтобы позволить функции сообщать свой результат не как значение, а через свой результат функции type. Например так:
// 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-due-to-reference является явный выбор постоянной времени компиляции.
Надеюсь, проблема oops-there-was-a-reference-connected-in-your-constexpr
будет исправлена в C ++ 17, но до тех пор макрос, подобный приведенному выше STATIC_N_ITEMS
, обеспечивает переносимость, например компиляторам clang и Visual C ++, сохраняя безопасность типов.
Связано: макросы не учитывают области, поэтому, чтобы избежать конфликтов имен, может быть хорошей идеей использовать префикс имени, например MYLIB_STATIC_N_ITEMS
.
person
Cheers and hth. - Alf
schedule
16.09.2011
std::array
s,std::vector
s иgsl::span
s - я, честно говоря, ожидал, что в FAQ о том, как использовать массивы в C ++, будет сказано, что к настоящему времени вы можете начать рассматривать просто, ну, не их использование. - person einpoklum   schedule 19.03.2017