5. Поширені підводні камені при використанні масивів.
5.1 Провалля: довіряти небезпечному типу.
Гаразд, вам сказали або ви самі зрозуміли, що глобальні дані (змінні області простору імен, до яких можна отримати доступ поза блоком перекладу) - це Evil ™. Але чи знали ви, наскільки справді вони 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-due-to-reference полягає в тому, щоб зробити вибір часу постійної компіляції явним.
Будемо сподіватися, що constexpr
випуск "oops-there-was-a-reference" - буде зафіксовано за допомогою C ++ 17, але до цього часу макрос, як STATIC_N_ITEMS
описано вище, забезпечує портативність, наприклад, до компіляторів clang і Visual C ++, зберігаючи тип безпека.
Пов’язано: макроси не поважають області застосування, тому, щоб уникнути зіткнень з іменами, може бути хорошою ідеєю використовувати префікс імені, наприклад MYLIB_STATIC_N_ITEMS
.