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

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

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

5.1.

Введение

101

В

этой

ситуации

компилятор

обычно

создает

Х

из

ma

ke ( )

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

в

х, тем самым отменяя (аннулируя) копирование

(сору elision).

В дополнение к инициализации именованных

объектов и объектов

в

сво­

бодной

памяти

конструкторы

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

для

инициализации

временных

объектов и для реализации явного преобразования типов.

За исключением "обычного конструктора", эти специальные

функции-чле­

ны

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

компилятором

по

мере

необходимости.

Если

вы

хотите

явно

затребовать

генерацию

реализаций

по

умолчанию,

можете

сделать

это

следу­

ющим

образом:

class

У

puЫic:

Y(Sometype);

Y(const

У&)

default;

//Я

хочу,

чтобы

компилятор сгенерировал

У(У&&)

11 ...

);

default;

11 //

копирующий конструктор по

умолчанию

и перемещающий конструктор

по умолчанию

Если

вы

явным

образом

указали

генерацию

некоторых

специальных

функций

по умолчанию,

дут..

то

прочие

определения

по

умолчанию

генерироваться

не

бу­

Если

у

класса

есть

член-указатель,

обычно

рекомендуется

явно

записывать

операции

копирования

и

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

Причина

в

том,

что

указатель

может

указывать

на

нечто,

что

класс

должен удалить,

и

в

этом

случае

выполняемое

по умолчанию почленное копирование будет неправильным. Или напротив - член может указывать на то, что класс не должен удалять. В любом случае читателю кода хотелось бы это точно знать. Пример можно найти в §5.2.1.

Хорошее эмпирическое правило (иногда называемое правwюм нуля) состо­ ит в том, чтобы определить либо все основные операции, либо ни одну из них

(используя

генерацию

по

умолчанию

для

всех

них).

Например:

struct

Z

 

 

Vector

 

 

string

);

 

 

Zzl;

 

Z

z2

= zl;

v; s;

// //

Инициализация Копирование zl

zl .v

.v и

и zl

zl .s

.

s

по умолчанию

по

умолчанию

В

этом

случае

компилятор

при

необходимости

будет

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

почленные

конструктор, копирующий

конструктор,

перемещающий конструктор

структор по умолчанию, и

все они будут

иметь правильную семантику.

и

де­

В

дополнение

к

=default

имеется

конструкция

=delete,

указывающая,

что

данная

операция

не

должна

генерироваться.

Базовый

класс

в

иерархии

104

Глава

5.

Основные

операции

Разрабатывая

класс,

мы

всегда

должны

рассматривать,

можно

ли,

и

как

именно,

копировать

объект.

Для

простых

конкретных

типов

почленное

копи­

рование

часто

оказывается

корректной

семантикой

копирования.

Для

некото­

рых

сложных

конкретных

типов,

таких

как

Vector,

почленное

копирование

является

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

семантикой

копирования;

для

абстрактных

типов

поч­

ленное

копирование

почти

никогда

не

является

правильной

семантикой.

5.2.1.

Копирование

контейнеров

Когда

класс

является

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

ресурса,

т.е.

отвечает

за

объект,

доступ

к

которому

осуществляется

через

указатель,

почленное

копирование

по

умол­

чанию

обычно

оказывается

катастрофой.

Почленная

копия

будет

нарушать

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

ресурса

(§3.5.2).

Например,

копирование

по

умолча­

нию

создало

бы

копию

Vector,

ссылающуюся

на

те

же

элементы,

что

и

ори­

гинал:

void

bad

copy(Vector

vl)

 

 

 

 

{

 

 

 

 

 

 

 

 

 

 

 

 

Vector

v2

=

vl;

//

Копирование

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

vl

 

vl [О]

 

2;

 

 

/ /

v2 [О]

теперь

тоже равно

2

!

 

v2[1]

=

З;

 

 

//

vl[l]

теперь

тоже равно

3!

в

v2

В предположении, что vl имеет может быть представлен следующим

четыре элемента, образом.

графически

результат

vl:

v2:

К

счастью,

тот

факт,

что

Vector

имеет

деструктор,

является

важной

подсказ­

кой что

о том, что семантика копирования по умолчанию (почленно) неверна и

компилятор должен хотя бы предупредить об этом. Нам нужно опреде­

лить лучшую семантику

копирования.

Копирование

объекта

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

коиструктором

и копирующим присваиваиием:

двумя

членами:

копирующи.w

class

Vector

private: douЬle*

elern;

//

elem

указывает

на

массив

из

sz

dоиЫе

int

sz;

 

puЫic:

 

 

11

Конструктор:

Vector(int

s);

установка

инварианта,

захват

ресурсов:

 

 

5.2. Копирование и перемещение

11 Деструктор: освобождение

ресурсов:

 

-Vector() (delete[] elem;)

 

 

 

Vector(const Vector& а);

 

//Копирующий

конструктор

Vector& operator=(const Vector&

а); //Копирующее

присваивание

105

douЬle& operator[J (int

i);

const douЬle& operator[] (int

int size()

const;

 

);

i)

const;

Корректное

определение

копирующего

конструктора

для

Vector

выделя­

ет

пространство

для

необходимого

количества

элементов,

а

затем

копирует

в

него

элементы,

так

что после

копирования

каждый

вектор

имеет

собствен­

ную

копию

элементов:

Vector::Vector(const Vector&

:elem (new

douЬle[a.sz] ),

sz (a.sz)

 

а)

//Копирующий

конструктор

// Выделение

памяти для элементов

for

(int i=O;

elem[i]

=

i a

1 =sz; ++i) .elem[i];

//

Копирование

элементов

Теперь результат примера v2=vl разом.

может

быть

представлен

следующим

об­

vl:

v2:

Конечно, в дополнение

рующее присваивание:

к

копирующему

конструктору

нам

требуется

копи­

Vector&

Vector:

:operator=(const

Vector&a)//

Копирующее присваивание

(

 

 

 

 

 

 

 

 

 

douЫe* р

=

new

 

douЬle[a.sz];

 

 

for

 

(int

i=O; i

1

=a.sz; ++i)

 

 

 

 

p[i]

=

a.elem[i];

 

 

 

delete[]

elem;

 

 

//Удаление

старых элементов

elem

= р;

 

 

 

 

 

 

 

sz

=

a.sz;

 

 

 

 

 

 

return *this;

 

 

 

 

 

Имя

this в функциях-членах является предопределенным

объект,

для которого вызывается эта функция-член.

указателем

на

5.2.

Копирование

и

перемещение

107

При

наличии

такого

определения,

чтобы

реализовать

передачу

возвращаемо­

го

значения

из

функции,

компилятор

выберет

перемещающий

конструктор.

Это

означает,

что

r=x+y+z

не

будет

содержать

копирования

векторов;

вместо

этого векторы просто перемещаются.

Как обычно, определение перемещающего

конструктора

Vector

триви­

ально:

Vector::Vector(Vector&&

а)

 

: elem{ а. elem),

//

"забираем"

sz{a.sz)

 

 

 

 

{

 

 

 

 

a.elem

= nullptr;

//

Теперь

в а

a.sz =

О;

 

 

 

элементы из а

нет элементов

&& означает "ссылка на r-значение" и является связать r-значение. Слово "r-значение" (rvalue)

ссылкой, является

с которой мы дополнением

можем к 1-зна­

чению

(lvalue),

которое

грубо

означает

"нечто,

что

может

появиться

в

левой

части

присваивания".

Таким

образом,

r-значение

в

первом

приближении

яв­

ляется

значением,

которому

нельзя

выполнить

присваивание,

например

целое

число,

возвращаемое

вызовом

функции.

Таким

образом,

r-ссылка является

ссылкой

на

нечто,

чему

никто

иной

ничего

не

может

присвоить,

поэтому

мы

можем

безопасно

"украсть"

ее

значение.

Примером

может

служить

локальная

переменная res в operator+ ()

в примере с Vector.

Перемещающий конструктор

не принимает константный аргумент:

в

ко­

нечном счете

перемещающий

конструктор

должен

удалить

значение

из

своего

аргумента. Аналогично определяется и перемещающее присваивание.

Операция перемещения применяется, когда r-ссылка используется

в

каче­

стве инициализатора или как правая сторона присваивания.

После перемещения перемещаемый объект должен находиться

в

состоя­

нии,

которое

позволяет

выполнить

деструктор.

Как

правило,

мы

также

раз­

решаем присваивание перемещенному мы стандартной библиотеки (глава 12,

объекту. Это предполагают "Алгоритмы"). Наш Vector

и алгорит­ поступает

именно таким образом.

 

Когда программист знает, что

компилятор недостаточно

умен,

значение больше не чтобы это понять,

будет использоваться, но программист может ука­

зать

ему

на

этот

факт:

Vector f ()

 

{

 

Vector

х(1000);

Vector

у(2000);

Vector

z(ЗООО);

11 Получаем копию

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

в

f()

позже):

z

=

х;

5.3.

Управление ресурсами

thread t{heartbeat};

 

11 Перемещение t в my_threads

(§13.2.2):

my_threads.push_back(std::move(t}};

109

/ / . . . Vector

Другие

инициализации

vec(n);

 

for

(int i=O;

i!=vec.size(); ++i)

 

vec[i]

=

777;

 

return vec;

 

//

Перемещение

vec из

init()

auto

v

=

init(l'000'000);

//Запуск

heartbeat

и

инициализация

v

Во

многих

случаях

дескрипторы

ресурсов,

такие

как

Vector

и

thread,

яв­

ляются

превосходными

альтернативами

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

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

встроенных

указателей.

Фактически

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

указатели

стандарт­

ной

библиотеки,

такие

как

unique

_ptr,

тоже

являются

дескрипторами

ре­

сурсов(§13.2.1 ).

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

vector

стандартной

библиотеки

для

хранения

потоков

thread,

потому

что

мы

не

можем

параметризовать

наш

простой

Vector

ти­

пом

элементов

(до

§6.2).

Во

многом

так

же,

как

new

и

delete

исчезают

из

кода

приложения,

мы

мо­

жем

заставить

исчезнуть

в

дескрипторах

ресурсов

указатели.

В

обоих

случаях

результат

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

собой

более

простой

и

удобный

в

обслуживании

код

без

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

накладных

расходов.

В

частности,

мы

можем

добиться

строгой

безопасности

ресурсов,

т.е.

устранить

утечки

ресурсов

для

общего

понятия

ресурса.

Примерами

являются

объекты

vector,

хранящие

память,

thread, хранящие системные потоки, и fstream, хранящие файлы. Во многих языках управление ресурсами в основном делегируется

сбор­

щику мусора. С++ также предлагает

интерфейс сборки мусора, так

можете подключить сборщик мусора.

Однако лично я считаю сборку

что вы мусора

последним

вариантом

-

после

исчерпания

более

ясных,

более

общих

и

луч­

ше

локализованных

альтернатив

управления

ресурсами.

Мой

идеал

заклю­

чается

в

том,

чтобы

не

создавать

мусор,

тем

самым

устраняя

необходимость

в

сборщике

мусора:

не

мусори!

Сборка

мусора

фундаментально

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

собой

глобальную

схему

управления

памятью.

Умные

реализации

могут

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

эту глобаль­

ность,

но

по

мере

того,

как системы

становятся

все

более

и

более

распреде­

ленными

(вспомните

о

кешах,

многоядерности

и

кластерах),

локальность

ста­

новится важнее, чем когда-либо ранее.

Кроме того, память не является единственным

ресурсом.

Ресурс

-

это

все,

что

должно

быть

захвачено

и (явно

или

неявно)

освобождено

после

ис-