Добавил:
t.me Опубликованный материал нарушает ваши авторские права? Сообщите нам.
Вуз: Предмет: Файл:

2 семестр / Литература / Язык программирования С++. Краткий курс. Страуструп

.pdf
Скачиваний:
9
Добавлен:
16.07.2023
Размер:
31.34 Mб
Скачать

230

Глава

13.

Утилиты

13.1.

Введение

Не четко

все компоненты стандартной

библиотеки входят

в состав

средств с

помеченным функциональным

предназначением,

таким как

"контей­

неры" или

"ввод-вывод". В этом

разделе приводится

небольших

широко используемых

компонентов. Такие

несколько примеров компоненты (классы

и

шаблоны)

часто

называют

словарными

типами

(vocabulary types),

потому

что

они

являются

частью

общего

словаря,

который

мы

используем

для

опи­

сания

наших

проектов

и

программ.

Такие

библиотечные

компоненты

часто

выступают

в

качестве

строительных

блоков

для

более

мощных

библиотечных

средств,

включая

другие

компоненты

стандартной

библиотеки.

Чтобы

быть

полезными,

функция

или тип

не

обязаны

быть

сложными

или

тесно

связан­

ными

с

массой

других

функций

и

типов.

13.2.

Управление

ресурсами

Одной

из

ключевых

задач

любой

нетривиальной

программы

является

управление

ресурсами.

Ресурс

-

это

то,

что

должно

быть

захвачено,

а поз­

же

(явно

или

неявно)

освобождено.

Примерами

являются

память,

блокиров­

ки,

сокеты,

дескрипторы

потоков

выполнения

и

файловые

дескрипторы.

Для

длительно

работающей

программы

неспособность

своевременно

освободить

ресурс

("утечка")

может

привести

к

серьезному

снижению

производительно­

сти

и,

возможно,

даже

к

аварийному

завершению

программы.

Даже

для

ко­

ротких

программ

утечка

может

стать

помехой,

например

из-за

нехватки

ре­

сурсов, увеличивающей время выполнения на порядки.

Компоненты стандартной библиотеки разработаны

так,

чтобы

не

допус­

кать

утечки

ресурсов.

Для

этого

они

полагаются

на базовую

языковую

под­

держку

управления

ресурсами

с

использованием

пар

"конструктор/деструк­

тор",

чтобы

гарантировать,

что

ресурс

не

переживет

объект,

ответственный

за

него.

Примером

является

использование

пары

"конструктор/деструктор"

в

Vector

для управления

временем

жизни

его

элементов

(§4.2.2),

и

все

кон­

тейнеры

стандартной

библиотеки

реализованы

аналогичным

образом.

Важно

отметить,

что

этот

подход

корректно

взаимодействует

с

обработкой

ошибок

с

использованием

исключений.

Например,

этот

метод

используется

для

классов

блокировки

стандартной

библиотеки:

mutex

11 ...

m;

/ / //

Используется

для защиты доступа

к совместно

используемым данным

void

f

()

232

Глава

13.

Утилиты

могли

бы

решить

проблему,

просто

не

используя

указатель

и

не

используя

new:

void

f(int

i,

int

{

 

 

 

Х

х;

 

 

11 ...

 

 

К

сожалению,

j)

//Использование

локальной

переменной

злоупотребление

оператором

new

также

указателями

ссылками), похоже, становится все более и более серьезной проблемой.

и

Однако, когда

вам действительно нужна

ptr оказывается

очень легким механизмом

семантика указателей, unique

без накладных расходов памяти

и

времени

по

сравнению

с

правильным

применением

встроенного

указате­

ля.

Его

применение

включает

передачу объектов

из

динамической

памяти

в

функции

и

их

возврат

из

них:

//

Создает

Х

и

передает

его

в

unique_ptr:

unique_ptr<X> make_X(int

i)

{

 

 

 

//

... проверка i

и

прочие

действия

return

unique_ptr<X>{new

X{i));

unique

_ptr

представляет

собой

дескриптор

отдельного

объекта

(или

мас­

сива)

почти

так

же,

как

vector

представляет

собой

дескриптор

последова­

тельности

объектов.

Оба

они

управляют

временем

жизни

других

объектов

(используя

идиому

RAII)

и

оба

используют

семантику

перемещения,

чтобы

сделать

возврат

с

помощью

return

простым

и

эффективным.

shared_ptr

похож

на

unique_ptr,

с

тем

отличием,

что

shared_ptr

ко­

пируются,

а

не

перемещаются.

Интеллектуальные

указатели

shared_ptr

для

объекта

совместно

используют

владение

этим

объектом;

объект

уничтожает­

ся,

когда

уничтожается

последний

из

указывающих

на

него

shared_ptr.

На­

пример:

void void

f(shared_ptr<fstream>); g(shared_ptr<fstream>);

void

user(const

string&

name,

ios_base::openmode

mode)

shared_ptr<fstream>

fp

{new

if

( ! *fp)

// Убеждаемся,

 

throw

No_file{);

 

 

fstream(name ,mode));

что файл корректно открыт

f (fp); g(fp);

11

...

236

Глава

13.

Утилиты

Состояние

удаленного

объекта

в

общем

случае

не

определено,

но

все

типы

стандартной

библиотеки

оставляют

перемещенный

объект

в

состоянии,

в ко­

тором

он

может

быть

уничтожен

и

присвоен.

Было

бы

неразумно

не

следо­

вать

этому

примеру.

Для

контейнера

(например,

вектора

или

строки)

состоя­

ние

после

перемещения

будет

"пустым

контейнером".

Для

многих

типов

пу­

стое

состояние

представляет

собой

значение

по

умолчанию:

оно

имеет

смысл

и

дешево

устанавливается.

Передача

аргументов

является

важным

вариантом

использования,

который

требует

перемещения

(§7.4.2).

Иногда

мы

хотим

передать

набор

аргументов

другой

функции,

ничего

не меняя

(для

достижения

"прямой

передачи"):

template<typename Т, typename

... Args>

unique_ptr<T> make_unique(Args&& ... args)

{

 

 

/ /

Передача каждого аргумента:

return unique_ptr<T>{new

T{std::forward<Args>(args)

...

));

Функция

forward

()

стандартной

библиотеки

отличается

от

более

про­

стой

s

td:

: move

( ) ,

правильно

обрабатывая

тонкости,

связанные

с

1-

и

r-зна­

чениями

(§5.2.2).

Используйте

std::

forward

()

исключительно

для

пере­

дачи

и не

передавайте

что-либо

дважды;

как

только

вы

передали

объект,

вы

больше

не

можете

его

использовать.

13.3.

Проверка выхода за границы

диаnаэона:gsl: :span

Традиционно

ошибки

выхода

за

границы

диапазона

были

основным

источ­

ником

серьезных

ошибок

в

программах

на

С

и

С++.

Использование

контей­

неров

(глава

11,

"Контейнеры"),

алгоритмов

(глава

12,

"Алгоритмы")

и

цикла

for

по

диапазону

значительно

уменьшили

эту

проблему,

но

можно

сделать

больше.

Основным

источником

ошибок

выхода

за

границы

диапазона

явля­

ется

то,

что

люди

передают

указатели

(обычные

или

интеллектуальные),

а

за­

тем,

чтобы

узнать

количество

элементов,

на

которые

они

указывают,

полага­

ются

на

соглашение.

Наилучший

совет

для

кода

вне

дескрипторов

ресурсов

состоит

в

том,

чтобы

предположить,

что

указатель

указывает

не

более

чем

на

один

объект

[CG:F.22],

но

без

соответствующей

поддержки

этот

совет

мало

что

дает.

Нам

может

помочь

string

view

(§9.3)

из

стандартной

библиоте­

ки,

но

это

представление

доступно

только

для

чтения

и

только

для

символов.

Большинству программистов нужно большее.

В работе [61] предлагаются некоторые рекомендации

и

небольшая

библи­

отека

для

их

поддержки

[23],

включая

тип

span

для

ссылки

на

диапазон

эле-

13.3.

Проверка

выхода

за

границы

диапазона:

gsl::span

237

ментов.

Этот

span

предложен

в

стандарт,

но

пока

что

это

просто

код,

который

вы можете загрузить, если он вам нужен.

string_span представляет собой пару

(указатель,

длина),

обозначающую

последовательность

элементов.

span<int>:

{begin(),

siz

е()}

Целыечисла:

+----------------- --

~l_!~2~!_з~!_s~!_в~_1_з~2_1~34~J_ss~I

span

предоставляет

доступ

к

непрерывной

последовательности

элемен­

тов.

Элементы

могут

храниться

разными

способами,

в

том

числе

в

векторах

и

встроенных

массивах.

Как

и

указатель,

span

не

владеет

символами,

на

кото­

рые

указывает.

В

этом

он

похож

на

string

_

view

(§9.3)

и

на

пару

итераторов

STL 12.3). Рассмотрим

общий

стиль интерфейса:

void

fpn(int*

(

 

 

 

for

(int

 

 

р [i]

р,

i

О;

int

О;

n)

i<n;

++i)

Мы

предполагаем,

что

р

указывает

на

n

целых

чисел.

К

сожалению,

это

предположение

-

просто

соглашение,

поэтому

мы

не

можем использовать

его

для

написания цикла

for

для

диапазона,

а

компилятор

не может реализо­

вать

дешевую

и

эффективную

проверку

выхода за

границы

диапазона.

Кроме

того,

наше

предположение

может

попросту

быть

неверным:

void

use(int х)

(

 

 

int а[100];

 

fpn(a,100);

 

fpn(a,1000);

 

fpn(a+l0,100);

 

fpn (а, х) ;

// ОК

 

 

 

//Просто

опечатка!

(Ошибка

//Ошибка

диапазона

в

fpn

/ / Выглядит невинно,

 

но . ..

диапазона

в

fpn)

Можно

сделать

лучше,

если

использовать

span:

void

fs(span<int>

(

 

 

 

 

 

for

(int

х

:

 

 

х =

О;

 

р)

р)

Использовать

f

s

можно,

например,

так:

238

Глава

13. Утилиты

void

use(int х)

{

 

 

int a[lOOJ;

 

fs(a);

 

fs(a,1000);

 

fs({a+l0,100});

 

f s ( {а, х} ) ;

//Неявное

создание

span<int>{a,100}

//

Ошибка

:ожидается

span

//

Ошибка

диапазона

в fs

/ /

Очевидно подозрительно

То

есть

распространенный

случай

создания

span

непосредственно

из

мас­

сива теперь

безопасен

(компилятор

сам

вычисляет

количество

элементов)

и

прост

для

записи.

В

других

случаях

вероятность

ошибок

снижается,

потому

что программист должен явно составить диапазон.

Распространенный случай, когда диапазон передается

от

функции

к

функ­

ции, оказывается проще, чем для интерфейсов

но, не требует дополнительной проверки:

(указатель,счетчик),

и,

очевид­

void

fl(span<int>

void

f2(span<int>

{

 

 

11 ...

 

f1 (р);

р); р)

При

использовании

для

индексации

(например,

r

[

i])

выполняется

про­

верка

выхода

за

границы

диапазона,

и

в

случае

ошибки

генерируется

исклю­

чение

gsl::

fail

_

fast.

Проверка

выхода

за

границы

диапазона

может

быть

подавлена

для

кода,

критичного

к

производительности.

Когда

span

станет

стандартом,

я

ожидаю,

что

s

td:

:

span

будет

использовать

контракты

(20]

для управления ответами на нарушения границ диапазона.

 

Обратите внимание, что для цикла необходима только

одна

проверка

диа­

пазона.

Таким

образом,

для

распространенного

случая,

когда

тело

функции,

использующей

span,

представляет

собой

цикл

по

диапазону

span,

эта

про­

верка оказывается практически бесплатной.

Диапазон символов

поддерживается

непосредственно

и

называется

gsl:

:

string

_

span.

13.4.

Специализированные

контейнеры

не

Стандартная

библиотека

предоставляет несколько контейнеров,

которые

вписываются

идеально в

каркас STL (глава 11, "Контейнеры",

глава 12,

"Алгоритмы").

Примерами

являются

встроенные

массивы,

array

и

string.

Я

иногда

называю

их

"почти

контейнерами",

но

это

не

совсем

справедливо:

они

содержат

элементы,

поэтому

являются

контейнерами,

но

у каждого есть

ограничения

или

дополнительные

возможности,

которые

делают

их

выпада-

13.4.

Специализированные

контейнеры

239

ющими из

контекста

сание STL.

 

STL.

Кроме

того,

их

отдельное

описание

упрощает

опи­

T[N]

array<T,N>

Ьitset<N> vector<bool>

"Почти контейнеры"

Встроенный массив: непрерывная последовательность фиксиро­

 

ванного размера из N элементов типа т; неявно преобразуется в

Т*

Непрерывная последовательность фиксированного размера из N

элементов типа т; подобна встроенному массиву, но с решением

 

большинства проблем

 

Последовательность фиксированного размера из N битов

 

Последовательность битов, компактно хранящаяся в специализа­

ции vector

 

pair<T,U>

tuple<T ...

>

Два элемента типов т и u

Последовательность

произвольного количества элементов

произвольных типов

 

basic

_

string<C>

Последовательность символов типа с; предоставляет строковые операции

valarray<T>

Массив числовых значений типа т; операции

предоставляет

числовые

Почему

стандартная

библиотека

предоставляет

так

много

контейнеров?

Они

служат

распространенным,

но

различным

(хотя

и

часто

перекрываю­

щимся)

потребностям.

Если

бы

стандартная

библиотека

их

не

предоставляла,

многим

программистам

пришлось

бы

разрабатывать

и

реализовывать

соб­

ственные

контейнеры.

pair и

tuple -

гетерогенные; все прочие контейнеры гомогенные

(все их

элементы

одного и того же типа).

 

 

Элементы array, vector и tuple располагаются в памяти непрерыв­

но; forward_ list и map

представляют

собой связанные структуры.

bi tset и vector<bool>

хранят биты и

обращаются

к ним через прок­

си-объекты; все прочие

контейнеры стандартной библиотеки могут хра­

нить различные типы и

непосредственно обращаться

к хранимым эле­

ментам.

 

 

 

 

 

basic_ string требует,

чтобы все были некоторой

разновидностью

символов и обеспечивали

возможность работы со строками, как, напри­

мер, конкатенация или операции, чувствительные к локали.

valarray требует, чтобы

все элементы

были числами и обеспечивали

возможность выполнения числовых операций.