КулЛиб - Классная библиотека! Скачать книги бесплатно 

Эффективный и современный C++ [Скотт Мейерс] (pdf) читать онлайн

Книга в формате pdf! Изображения и текст могут не отображаться!


 [Настройки текста]  [Cбросить фильтры]
Эффективный
и современныи
""

С++

Effective Modern С++

Scott Meyers

lkijing



Cambridgc



1:arnham



Kбln



Scbastopol



Tokyo



O"J{cill;

O"REILLY@

Эффективный
и современныи
С++
w

42 рекомендации по использованию С++ 11 и С++14

Скотт Мейерс


Москва • Санкт-Петербург• Киев
2016

ББК 32.973.26-018.2.75

М45
УДК 681.3.07
Издател1.ский дом "Вильяме"
Зав. редакцией С.Н. Триrуб
Перевод с английского и редакция канд. техн. наук И.В. Красикова
По общим вопросам обращайтесь в Издательский дом "Вильяме"

110

адресу:

i11fo@williamspнЫishi11g.rom, IНtp:/ /www.williamspt1Ьlisl1i11g.roш
Мейерс, Скотт.
Эффективный и современный С++:

М45

и С++14.: Пер. с англ.
т

-

42 рекомендации по исполыованию С++ 1 1
М. : ООО "ИЛ. Вильяме", 2016. - 304 с.: ил. - Пap required
to know thefull definition ofT?'' Дополнительный пример Matrix в разделе 5.3 основан на пись­
ме Дэвида Абрахамса (David Abrahams). Комментарий Джо Аргонна (Joe Argonne) от 8 декаб­
ря 20 12 года к материалу из блога "Another a/ternative to lambda move capture" от 30 ноября
2013 года стал источником для описанного в разделе 6.2 подхода к имитации инициализации
на основе std:: Ьind в C++l l. Пояснения в разделе 7.3 проблемы с неявным отключением
в деструкторе std: : thread взяты из статьи Ганса Бехма (Hans-J. Boehm) "N2802: А plea to
reconsider detach-on-destruction for thread objects" от 4 декабря 2008 года. Раздел 8. 1 появился
благодаря обсуждению материала в блоге Дэвида Абрахамса "Want speed? Pass Ьу value" от
15 августа 2009 года. Идея о том, что типы, предназначенные только для перемещения, долж­
ны рассматриваться отдельно, взята у Мэттью Фьораванте (Matthew Fioravante), в то время
как анализ копирования на основе присваивания взят из комментариев Говарда Хиннанта
(Howard Нinnant). В разделе 8.2 Стивен Т. Лававей и Говард Хиннант помогли мне понять
вопросы, связанные с относительной производительностью функций размещения и встав­
ки, а Майкл Винтерберг (Michael Winterberg) привлек мое внимание к тому, как размещение
может приводить к утечке ресурсов. (Майкл, в свою очередь, называет своим источником
презентацию "С++ Seasoning" Шона Парента (Sean Parent) на конференции Going Native 2013.
Майкл также указал, что функции размещения используют непосредственную инициализа­
цию, в то время как функции вставки используют копирующую инициализацию.)
Проверка черновиков технической книги является длительной и критичной, но со­
вершенно необходимой работой, и мне повезло, что так много людей были готовы за
нее взяться. Черновики этой книги были официально просмотрены такими специали­
стами, как Кассио Нери (Cassio Neri), Нейт Кёль (Nate Kohl), Герхард Крейцер (Gerhard
Kreuzer), Леон Золман (Leor Zolman), Барт Вандевойстин (Bart Vandewoestyne), Стивен Т.
Лававей (Stephan Т. Lavavej), Невин Либер (Nevin ":- )" Liber ) , Речел Ченг (Rachel Cheng),
Роб Стюарт (Rob Stewart), Боб Стигалл (ВоЬ Steagall), Дамьен Уоткинс (Damien Watkins),
Брэдли Нидхам (Bradley Е. Needham), Рейнер Гримм (Rainer Grimm), Фредрик Винклер
(Fredrik Winkler), Джонатан Уокели (Jonathan Wakely), Герб Саттер (Herb Sutter), А ндрей

Благодарности

13

Александреску (Andrei Alexandrescu), Эрик Ниблер (Eric NieЬler), Томас Беккер (Thomas
Becker), Роджер Орр (Roger Оп), Энтони Вильяме (Anthony Williams), Майкл Винтерберг
(Michael Winterberg), Бенджамин Хахли (Benjamin Huchley), Том Кирби-Грин (Tom Кirby­
Green), Алексей Никитин (Alexey А. Nikitin), Вильям Дилтрай (William Dealtry), Хуберт
Мэттьюс (Hubert Matthews) и Томаш Каминьски (Tomasz Кaminski). Я также получил от­
зывы ряда читателей с помощью сервисов O'Reilly's Early Release EBooks и Safari Books
Online's Rough Cuts, посредством комментариев в моем блоге ( The View from Aristeia)
и электронной почтой. Я благодарен каждому, кто высказал свои замечания. Эта книга
получилась гораздо лучше, чем она была бы без этой помощи. В особенности я признате­
лен Стивену Т. Лававею и Робу Стюарту, чьи чрезвычайно подробные и всеобъемлющие
замечания заставили меня забеспокоиться: кто из нас потратил больше сил и времени
на эту книгу - я или они? Моя особая благодарность - Леору Золману (Leor Zolman),
который не только просмотрел рукопись, но и дважды проверил все приведенные в ней
примеры кода.
Черновики цифровых версий книги были подготовлены Герхардом Крейцером
(Gerhard Kreuzer), Эмиром Вильямсом (Emyr Williams) и Брэдли Нидхэмом (Bradley Е.
Needham).
Мое решение ограничить длину строки кода 64 символами (максимум для правиль­
ного отображения на печати, а также на различных цифровых устройствах при разной
ориентации и конфигурации шрифтов) было основано на данных, предоставленных
Майклом Махером (Michael Maher).
С момента первой публикации я исправил ряд ошибок и внес некоторые усовершен­
ствования, предложенные такими читателями, как Костас Влахавас (Kostas Vlahavas), Да­
ниэль Алонсо Алеман (Daniel Alonso Alemany), Такатоши Кондо (Takatoshi Kondo), Бар­
тек Сургот (Bartek Szurgot), Тайлер Брок (Tyler Brock), Джай Ципник (Jay Zipnick), Барри
Ревзин (Вапу Revzin), Роберт Маккейб (Robert МсСаЬе), Оливер Брунс (Oliver Bruns), Фа­
брис Ферино (Fabrice Ferino), Дэнез Джонитис (Dainis Jonitis), Петр Валашек (Petr Valasek)
и Барт Вандевойстин (Bart Vandewoestyne). Большое спасибо всем им за помощь в повы­
шении точности и ясности изложенного материала.
Эшли Морган Вильяме (Ashley Morgan Williams) готовила отличные обеды у себя
в Lake Oswego Pizzicato. Им (и ей) нет равных.
И более двадцати лет моя жена, Нэнси Л. Урбано (Nancy L. Urbano), как обычно во
время моей работы над новой книгой, терпит мою раздражительность и оказывает мне
всемерную поддержку. В ходе написания книги постоянным напоминанием о том, что за
пределами клавиатуры есть другая жизнь, служила мне наша собака Дарла.

14

Благодарности

Вв еден и е

Если вы - опытный программист на языке программирования С++, как, например,
я, то, наверное, первое, о чем вы подумали в связи с С++ 1 1 , - "Да, да, вот и он - тот же
С++, только немного улучшенный". Но познакомившись с ним поближе, вы, скорее все­
го, были удивлены количеством изменений. Объявления auto, циклы for для диапазо­
нов, лямбда-выражения и rvаluе-ссылки изменили лицо С++, - и это не говоря о новых
возможностях параллельности. Произошли и идиоматические изменения. О и t ypede f
уступили место nullptr и объявлениям псевдонимов. Перечисления получили области
видимости. Интеллектуальные указатели стали предпочтительнее встроенных; переме­
щение объектов обычно предпочтител ьнее их копирования.
Даже без упоминания С++ 14 в С++ 1 1 есть что поизучать.
Что еще более важно, нужно очень многое изучить, чтобы использовать новые воз­
можности эффективно. Если вам нужна базовая информация о "современных" возмож­
ностях С++, то ее можно найти в избытке. Но если вы ищете руководство о том, как
использовать эти возможности для создания правильного, эффективного, сопровождае­
мого и переносимого программного обеспечения, поиск становится более сложным. Вот
здесь вам и пригодится данная книга. Она посвящена не описанию возможностей С++ 1 1
и C++l4, а их эффективному применению.
Информация в книге разбита на отдельные разделы, посвященные тем или иным ре­
комендациям. Вы хотите разобраться в разных видах вывода типов? Или хотите узнать,
когда следует (а когда нет) использовать объявление auto? Вас интересует, почему функ­
ция-член, объявленная как const, должна быть безопасна с точки зрения потоков, как
реализовать идиому Pimpl с использованием st d: : un i que_pt r, почему следует из­
бегать режима захвата по умолчанию в лямбда-выражениях или в чем различие между
st d : : а torniс и volа tilе? Ответы на эти вопросы вы найдете в книге. Более того, эти
ответы не зависят от платформы и соответствуют стандарту. Это книга о переносимом С++.
Разделы книги представляют собой рекомендации, а не жесткие правила, поскольку
рекомендации имеют исключения. Наиболее важной частью каждого раздела является
не предлагаемая в нем рекомендация, а ее обоснование. Прочитав раздел, вы сможете
сами определить, оправдывают ли обстоятельства вашего конкретного проекта отход
от данной рекомендации. Истинная цель книги не в том, чтобы рассказать вам, как надо
поступать или как поступать не надо, а в том, чтобы обеспечить вас более глубоким по­
ниманием, как та или иная концепция работает в С++ 1 1 и С++ 1 4.

Терминоnо r ия и соrnашения
Чтобы мы правильно понимали друг друга, важно согласовать используемую терми­
нологию, начиная, как ни странно это звучит, с термина "С++". Есть четыре официаль­
ные версии С++, и каждая именуется с использованием года принятия соответствующего
стандарта ISO: С++98, С++ОЗ, C++l 1 и С++14. С+ +98 и С++ОЗ отличаются один от друго­
го только техническими деталями, так что в этой книге обе версии я называю как С++98.
Говоря о С++ 1 1 , я подразумеваю и С++ 1 1 , и С++ 1 4, поскольку С++ 14 является надмно­
жеством С++ 1 1 . Когда я пишу "С++ 1 4': я имею в виду конкретно С++ 14. А если я просто
упоминаю С++, я делаю утверждение, которое относится ко всем версиям языка.
Использованный термин

Подразумеваемая версия

(++

Все

(++98

с++98

и

с++03

(++11

(++11

и

(++14

(++14

(++14

В результате я мог бы сказать, что в С++ придается большое значение эффективности
(справедливо для всех версий), в С++98 отсутствует поддержка параллелизма (справед­
ливо только для С++98 и С++ОЗ), С++ 1 1 поддерживает лямбда-выражения (справедливо
для C++l l и С++14) и С++14 предлагает обобщен ный вывод возвращаемого типа функ­
ции (справедливо только для С++ 1 4).
Наиболее важной особенностью С++ 1 1 , вероятно, является семантика перемещения,
а основой семантики перемещения является отличие rvа/ие-выражени й от /vа/uе-выра­
жений. Поэтому rvalue указывают объекты, которые могут быть перемещены, в то время
как lvalue в общем случае перемещены быть не могут. Концептуально (хотя и не всегда
на практике), rvalue соответствуют временным объектам, возвращаемым из функций,
в то время как lvalue соответствуют объектам, на которые вы можете ссылаться по име­
ни, следуя указателю или lvalue-ccылкe.
Полезной эвристикой для выяснения, является ли выражение lvalue, является ответ
на вопрос, можно ли получить его адрес. Если можно, то обычно это lvalue. Если нет, это
обычно rvalue. Приятной особенностью этой эвристики является то, что она помогает
помнить, что тип выражения не зависит от того, является ли оно lvalue или rvalue. Иначе
говоря, для данного типа Т можно иметь как lvalue типа Т, так и rvalue типа Т. Особенно
важно помнить это, когда мы имеем дело с параметром rvalue ссылочного типа, посколь­
ку сам по себе параметр является lvalue:
class Widget {
puЫ i c :
Widget (Widqet&& rhs); // rhs является lvalue, хотя
/ / и имеет ссьu�очный тип rvalue
};

16

Введение

Здесь совершенно корректным является взятие адреса rhs в перемещающем конструкто­
ре Widget, так что rhs представляет собой lvalue, несмотря на то что его тип - ссылка
rvalue. (По сходным причинам все параметры являются lvalue.)
Этот фрагмент кода демонстрирует несколько соглашений, которым я обычно следую.


Имя класса - Widget. Я использую слово Widget, когда хочу сослаться на произ­
вольный пользовательский тип. Если только мне не надо показать конкретные дета­
ли класса, я использую имя Widget, не объявляя его.



Я использую имя параметра rhs ("right-hand side'; правая сторона). Это предпочи­
таемое мною имя параметра для операций перемещения (например, перемещающего
конструктора и оператора перемещающего присваивания) и операций копирования
(например, копирующего конструктора и оператора копирующего присваивания). Я
также использую его в качестве правого параметра бинарных операторов:
Matrix operator+ ( const Matrix& lhs , const Matrix& rhз);
Я надеюсь, для вас не станет сюрпризом, что lhs означает "left-hand side" (левая
сторона).



использую специальное форматирование для частей кода или частей комментари­
ев, чтобы привлечь к ним ваше внимание. В перемещающем конструкторе Widget
выше я подчеркнул объявление rhs и часть комментария, указывающего, что rhs
представляет собой lvalue. Выделенный код сам по себе не является ни плохим, ни
хорошим. Это просто код, на который вы должны обратить внимание.



Я использую ".. . '; чтобы указать "здесь находится прочий код': Такое "узкое" трое­
точие отличается от широкого ... ·; используемого в исходных текстах шаблонов
с переменным количеством параметров в С++ 1 1 . Это кажется запутанным, но на са­
мом деле это не так. Вот пример.

Я

"

template
void processVal s ( const Ts& . . . params )

11
11
11
11
11

Эти троеточия
в исходном
тексте С++
Это троеточие означае т как ой -то код

Объявление processVals показывает, что я использую ключевое слово typename
при объявлении параметров типов в шаблонах, но это просто мое л ичное
предпочтение; вместо него можно использовать ключевое слово class. В тех
случаях, когда я показываю код, взятый из стандарта С++, я объявляю параметры
типа с использованием ключевого слова class, поскольку так делает стандарт.
Когда объект инициализирован другим объектом того же типа, новый объект яв­
ляется копией инициализирующего объекта, даже если копия создается с помощью пе­
ремещающего конструктора. К сожалению, в С++ нет никакой терминологии, которая
позволяла бы различать объекты, созданные с помощью копирующих и перемещающих
конструкторов:
Введение

17

void someFunc ( Widget w) ;

/ / Параметр w функции someFunc
11 передается по значению

Widget wid;

11 wid - объект класса Widget

someFunc ( wid) ;

11 В этом вызове someFunc w
/ / является копией wid , созданной
/ / копирующим конструктором

someFunc ( st d : : move ( wid) ) ; / / В этом вызове SomeFunc w
/ / является копией wid, созданной
/ / перемещающим конструктором

Копии rvalue в общем случае конструируются перемещением, в то время как копии
lvalue обычно конструируются копированием. Следствием является то, что если вы зна­
ете только то, что объект является копией друrого объекта, то невозможно сказать, на­
сколько дорогостоящим является создание копии. В приведенном выше коде, например,
нет возможности сказать, насколько дорогостоящим является создание параметра w, без
знания того, какое значение передано функции someFunc
rvalue или lvalue. (Вы также
должны знать стоимости перемещения и копирования Widget.)
В вызове функции выражения, переданные в источнике вызова, являются аргумен­
тами функции. Эти аргументы используются для инициализации параметров функции.
В первом вызове someFunc, показанном выше, аргументом является wid. Во втором вы­
зове аргументом является std ::move ( w i d) . В обоих вызовах параметром является w.
Разница между аргументами и параметрами важна, поскольку параметры являются lvalue,
но аргументы, которыми они инициализируются, могут быть как rvalue, так и lvalue. Это
особенно актуально во время прямой передачи, при которой аргумент, переданный функ­
ции, передается другой функции так, что при этом сохраняется его "правосторонность"
или "левосторонность". (Прямая передача подробно рассматривается в разделе 5.8.)
Хорошо спроектированные функции безопасны с тачки зрения исключений, что озна­
чает, что они обеспечивают как минимум базовую гарантию, т.е. гарантируют, что, даже
если будет сгенерировано исключение, инварианты программы останутся нетронутыми
(т.е. не будут повреждены структуры данных) и не будет никаких утечек ресурсов. Функ­
ции, обеспечивающие строгую гарантию, гарантируют, что, даже если будет сгенериро­
вано исключение, состояние программы останется тем же, что и до вызова функции.
Говоря о функциональном объекте, я обычно имею в виду объект типа, поддержи­
вающего функцию-член operator (). Другими словами, это объект, действующий, как
функция. Иногда я использую термин в несколько более общем смысле для обозначения
чего уrодно, что может быть вызвано с использованием синтаксиса вызова функции, не
являющейся членом (т.е. function Name (arguments) ). Это более широкое определе­
ние охватывает не только объекты, поддерживающие operator (), но и функции и ука­
затели на функции в стиле С. (Более узкое определение происходит из С++98, более ши­
рокое - из C++ll.) Дальнейшее обобщение путем добавления указателей на функции­
члены дает то, что известно как вызываемый объект (callaЫe object). Вообще говоря,
-

18

В ведение

можно иrнорировать эти тонкие отличия и просто рассматривать функциональные и вы­
зываемые объекты как сущности в С++, которые моrут быть вызваны с помощью неко­
торой разновидности синтаксиса вызова функции.
Функциональные объекты, создаваемые с помощью лямбда-выражений, известны как
замь1кания (closures). Различать лямбда-выражения и замыкания, ими создаваемые, при­
ходится редко, так что я зачастую rоворю о них обоих как о лямбдах (lambda). Точно так
же я редко различаю шаблоны функций (fuпction templates) (т.е. шаблоны, которые rе­
нерируют функции) и шаблонные функции (template ftшctions) (т.е. функции, сrенериро­
ванные из шаблонов функций). То же самое относится к шаблонам классов и шаблонным
классам.
Мноrие сущности в С++ моrут быть как объявлены, так и определены. Объявления
вводят имена и типы, не детализируя информацию о них, такую как их местоположение
в памяти или реализация:
extern int

х;

class Widget ;

11 Объявление объекта
11 Объявление класс а

bool func ( const Widget& W); 11 Объявление функции
enшn class Color;

11 Объявление перечисления
11 с областью видимости
11 ( см. раздел 3.4)

Определение предоставляет информацию о расположении в памяти и деталях реализации:
int

х;

cla ss Widget
)

11 Определение объекта
11 Определение класса

;

bool func ( const Widget& w)
{ return w . size ( ) < 1 0 ; 1 // Определение функции
enurn class Color
{ Yellow, Red, Blue f ;

11 Определение перечисления

Определение можно квалифицировать и как объявление, так что, если только то, что
нечто представляет собой определение, не является действительно важным, я предпочи­
таю использовать термин "объявление·:
Сигнатуру функции я определяю как часть ее объявления, определяющую типы
параметров и возвращаемый тип. Имена функции и параметров значения не име­
ют. В приведенном выше примере сиrнатура функции func представляет собой
bool (const Widget&) . Исключаются элементы объявления функции, отличные от ти­
пов ее параметров и возвращаемоrо типа (например. noexcept или constexpr, если
таковые имеются). (Модификаторы noexcept и constexpr описаны в разделах 3.8

В ведение

19

и 3.9.) Официальное определение термина "сигнатура" несколько отличается от моего,
но в данной книге мое определение оказывается более полезным. (Официальное опреде­
ление иногда опускает возвращаемый тип.)
Новый стандарт С++ в общем случае сохраняет корректность кода, написанного для бо­
лее старого стандарта, но иногда Комитет по стандартизации не рекомендует применять те
или иные возможности. Такие возможности находятся в "камере смертников" стандартиза­
ции и могут быть убраны из новых версий стандарта. Компиляторы могут предупреждать
об использовании программистом таких устаревших возможностей (но могут и не делать
этого), но в любом случае их следует избегать. Они могут не только привести в будущем
к головной боли при переносе, но и в общем случае они ниже по качеству, чем возмож­
ности, заменившие их. Например, st d : : a ut o _pt r не рекомендуется к применению
в C++ l l , поскольку std:: unique_pt r выполняет ту же работу, но лучше.
Иногда стандарт гласит, что результатом операции является неопределенное поведе­
ние (undefined behavior). Это означает, что поведение времени выполнения непредсказу­
емо, и от такой непредсказуемости, само собой разумеется, следует держаться подальше.
Примеры действий с неопределенным поведением включают использование квадратных
скобок ( []) для индексации за границами std: :vec t o r, разыменование неинициали­
зированного итератора или гонку данных (т.е. когда два или более потоков, как минимум
один из которых выполняет запись, одновременно обращаются к одному и тому же ме­
сту в памяти).
Я называю встроенный указатель, такой как возвращаемый оператором new, обычным
указателем (raw pointer). Противоположностью обычному указателю является интеллек­
туальный указатель (smart pointer). Интеллектуальные указатели обычно перегружают
операторы разыменования указателей (oper a t o r-> и opera t o r*), хотя в разделе 4.3
поясняется, что интеллектуальный указатель std::weak_ptr является исключением.

Зам е ча н ия и пр едл ож ен ия
Я сделал все возможное, чтобы книга содержала только ясную, точную, полезную ин­
формацию, но наверняка есть способы сделать ее еще лучшей. Если вы найдете в кни­
ге ошибки любого рода (технические, разъяснительные, грамматические, типографские
и т.д.) или если у вас есть предложения о том, как можно улучшить книгу, пожалуйста,
напишите мне по адресу ernc++@a r i st e i a . сот. В новых изданиях книги ваши замеча­
ния и предложения обязательно будут учтены.
Список исправлений обнаруженных ошибок можно найти по адресу ht tp://www.

a rist eia.corn/BookErra t a /ernc++-erra t a .htrnl.

О т р едакции
Редакция выражает признательность профессору университета Иннополис Е. Зуеву за
обсуждения и советы при работе над переводом данной книги.

20

В ведение

Жде м ва ши х отзывов!
Вы, читатель этой книги, и есть главный ее критик. Мы ценим ваше мнение и хотим
знать, что было сделано нами правильно, что можно было сделать лучше и что еще вы
хотели бы увидеть изданным нами. Нам интересны любые ваши замечания в наш адрес.
Мы ждем ваших комментариев и надеемся на них. Вы можете прислать нам бумажное
или электронное письмо либо просто посетить наш веб-сайт и оставить свои замечания
там. Одним словом, любым удобным для вас способом дайте нам знать, нравится ли вам
эта книга, а также выскажите свое мнение о том, как сделать наши книги более интерес­
ными для вас.
Отправляя письмо или сообщение, не забудьте указать название книги и ее авторов,
а также свой обратный адрес. Мы внимательно ознакомимся с вашим мнением и обяза­
тельно учтем его при отборе и подготовке к изданию новых книг.
Наши электронные адреса:
E-mail:
WWW:

i nfo@wi ll i amspuЫ i sh i ng.com
htt p://www . wi ll i ams puЫ i s h i ng . com

Наши почтовые адреса:
в России: 1 27055, Москва, ул. Лесная, д. 43, стр. 1
в Украине: 03 1 50, Киев, а/я 1 52

Введение

21

ГЛАВА 1

Выв од т и п о в

В С++98 имеется единственный набор правил вывода типов - для шаблонов функ­
ций. С++ 1 1 немного изменяет этот набор правил и добавляет два новых - для auto и для
decltype. С++ 14 расширяет контексты использования ключевых слов auto и decltype.
Все более широкое применение вывода типов освобождает вас от необходимости пра­
вильной записи очевидных или излишних типов. Он делает программы на С++ более
легко адаптируемыми, поскольку изменение типа в одной точке исходного текста авто­
матически распространяется с помощью вывода типов на другие точки. Однако он может
сделать код более сложным для восприятия, так как выводимые компилятором типы мо­
гут не быть настолько очевидными, как вам бы хотелось.
Без ясного понимания того, как работает вывод типов, эффективное программиро­
вание на современном С++ невозможно. Просто есть слишком много контекстов, в ко­
торых имеет место вывод типа: в вызовах шаблонов функций, в большинстве ситуаций,
в которых встречается ключевое слово aut o, в выражениях decltype и, начиная с С++ 14,
там, где применяется загадочная конструкция decltype (auto).
Эта глава содержит информацию о выводе типов, которая требуется каждому раз­
работчику на языке программирования С++. Здесь поясняется, как работает вывод типа
шаблона, как строится auto и как проходит свой путь decltype. Здесь даже объясняется,
как заставить компилятор сделать видимыми результаты своего вывода типа, чтобы убе­
диться, что компилятор выводит именно тот тип, который вы хотели.

1.1 . Вывод типа шаблона
Когда пользователи сложной системы не знают, как она работает, но их устраивает то,
что она делает, это говорит об удачном проектировании системы. Если мерить такой ме­
рой, то вывод типа шаблона в С++ является огромным успехом. Миллионы программис­
тов передают аргументы шаблонным функциям с вполне удовлетворительными резуль­
татами несмотря на то, что многие из этих программистов не способны на большее, чем
очень приближенное и расплывчатое описание того, как же были выведены эти типы.
Если вы относитесь к числу этих программистов, у меня для вас две новости - хоро­
шая и плохая. Хорошая новость заключается в том, что вывод типов для шаблонов явля­
ется основой для одной из наиболее привлекательных возможностей современного С++:
auto. Если вас устраивало, как С++98 выводит типы для шаблонов, вас устроит и то, как

С++ 1 1 выводит типы для auto. Плохая новость заключается в том, что когда правила
вывода типа шаблона применяются в контексте auto, они оказываются немного менее
интуитивными, чем в приложении к шаблонам. По этой причине важно действительно
понимать аспекты вывода типов шаблонов, на которых построен вывод типов для auto.
Этот раздел содержит информацию, которую вы должны знать.
Если вы готовы посмотреть сквозь пальцы на применение небольшого количества
псевдокода, то можно рассматривать шаблон функции как имеющий следующий вид:
template
void f(Param7YPE1 param) ;

Вызов может выглядеть следующим образом:
f (expr);

11 Вызов f

с

некоторым выражением

В процессе компиляции компилятор использует expr для вывода двух типов: типа
и типа ParamT ype. Эти типы зачастую различны, поскольку Pa ramType часто содер­
жит "украшения", например const или квалификаторы ссылки. Например, если шаблон
объявлен как
Т

ternplate
void f(const Т& param) ;

//

ParamType

-

const Т&

и мы осуществляем вызов
int

х

О;

f (х);

11 Вызов f с параметром int

то Т выводится как int, а ParamType
как const i n t & .
Вполне естественно ожидать, что тип, выведенный для Т , тот же, что и тип переданно­
го функции аргумента, т.е. что Т
это тип выражения expr. В приведенном выше при­
мере это так: х значение типа int и Т выводится как int. Но вывод не всегда работает
таким образом. Тип, выведенный для т, зависит не только от типа expr, но и от вида
Pa ramType. Существует три случая.
-

-

-



ParamType представляет собой указатель или ссылку, но не универсальную ссылку.
(Универсальные ссылки рассматриваются в разделе 5.2. Пока что все, что вам надо
знать, - что они существуют и не являются ни ссылками lvalue, ни ссылками rvalue.)



ParamType является универсальной ссылкой.



Pa ramType не является ни указателем, ни ссылкой.

Следовательно, нам надо рассмотреть три сценария вывода. Каждый из них основан
на нашем общем виде шаблонов и их вызова:
ternplate
void f(Param7YPE1 pararn) ;
f (expr);

24

/ / Вывод Т и

Глава 1. Вывод типов

ParamType

из

expr

Случай 1. Param7Y,pe я вляется указателе м или ссы лкой,
но не уни версальной ссылкой
Простейшая ситуация - когда Pa ramType является ссылочным типом или типом ука­
зателя, но не универсальной ссылкой. В этом случае вывод типа работает следующим
образом.
1 . Если типом expr является ссылка, ссылочная часть игнорируется.
2. Затем выполняется сопоставление типа expr с ParamType для определения

т.

Например, если у нас имеются шаблон
template
void f (T& param) ;
// param представляет собой ссыпку

и объявления переменных
11 х имеет тип int
int х = 27 ;
/
/ с х имеет тип const int
const int сх = х ;
const int& rx = х; // rx является ссыпкой на х как на const int

то выводимые типы для pa ram и Т в различных выводах будут следующими:
f ( х ) ; / / Т - int ,
тип param - int&
f ( cx) ; / / Т
const int , тип param - coпst int&
f ( rx ) ; / / Т - const int , тип param - const int&
-

Во втором и третьем вызовах обратите внимание, что, поскольку сх и rx объявлены
как константные значения, Т выводится как const int , тем самым приводя к типу па­
раметра const int &. Это важно для вызывающего кода. Передавая константный объект
параметру-ссылке, он ожидает, что объект останется неизменным, т.е. что параметр будет
представлять собой ссылку на const . Вот почему передача константного объекта в шаблон,
получающий параметр Т&, безопасна: константность объекта становится частью выведен­
ного для Т типа.
В третьем примере обратите внимание, что несмотря на то, что типом rx является
ссылка, тип т выводится как не ссылочный. Вот почему при выводе типа игнорируется
"ссылочность" rx.
Все эти примеры показывают ссылочные параметры, являющиеся lvalue, но вы­
вод типа точно так же работает и для ссылочных параметров rvalue. Конечно, rvаluе­
аргументы могут передаваться только ссылочным параметрам, являющимся rvalue, но
это ограничение никак не влияет на вывод типов.
Если мы изменим тип параметра f с Т & на const Т &, произойдут небольшие измене­
ния, но ничего удивительного не случится. Константность сх и rx продолжает соблю­
даться, но поскольку теперь мы считаем, что pa ram является ссылкой на const , const как
часть выводимого типа т не требуется:
template
void f (const Т& param) ; / / param является ссыпкой на const

1.1.

Вывод типа wабnона

25

27;
int х
const int СХ
const int& rx

1 1 К а к и ранее
1 1 Как и ранее
11 Как и ранее

=

=

х;
= х;

1 1 т - int , тип param - const int&
11 т
int , тип param - const int&
11 т
int , тип pa ram - const int &

f

(х) ;
f (сх) ;
f (rx) ;

Как и ранее, "ссылочность" rx при выводе типа игнорируется.
Если бы param был указателем (или указателем на const), а не ссылкой, все бы рабо­
тало, по сути, точно так же:
template
// Теперь param является указателем
void f (T* param ) ;
int х = 27 ;
const int *рх

11 Как и ранее
& х ; // рх - указатель на х, как на const int
11 Т - iпt ,
тип param - int*
// Т - const int, тип param - const int*

f

( &х ) ;
f (рх ) ;

Сейчас вы можете обнаружить, что давно усердно зеваете, потому что все это очень
скучно, правила вывода типов в С++ работают так естественно для ссылок и указателей,
что все просто очевидно! Это именно то, что вы хотите от системы вывода типов.

Случай 2. Param�e я вляется уни версальной ссылкой
Все становится менее очевидным в случае шаблонов, принимающих параметры, яв­
ляющиеся универсальными ссылками. Такие параметры объявляются как ссылки rvalue
(т.е. в шаблоне функции, принимающем параметр типа т, объявленным типом универ­
сальной ссылки является Т&&), но ведут себя иначе при передаче арrументов, являющих­
ся lvalue. Полностью вопрос рассматривается в разделе 5.2, здесь приводится его сокра­
щенная версия.


Если expr представляет собой lvalue, как Т, так и ParamType выводятся как \vа\uе­
ссылки. Это вдвойне необычно. Во-первых, это единственная ситуация в выводе
типа шаблона, когда Т выводится как ссылка. Во-вторых, хотя ParamType объяв­
лен с использованием синтаксиса rvаluе-ссылки, его выводимым типом является
\vаluе-ссылка.



Если expr представляет собой rvalue, применяются "обычные" правила (из случая l).

Примеры
template
void f ( T&& param) ; // param является универсальной ссылкой
int х = 27 ;
const int сх

26

х;

11 Как и ранее
1 1 Как и ранее

Гnава 1. Вывод типов

const int& rx

х; 11 Как и ранее

f (х) ;

f ( сх ) ;
f ( rx) ;
f

(27) ;

11
11
//
11
11
11
11
11

х - lvalue, так что Т - iпt & ,
тип pararn также является iпt&
с х - lvalue , так что Т - const iпt & ,
тип pararn также является coпst iпt&
rx - lvalue , так что Т - const iпt& ,
тип pararn также является const iпt&
2 7 - rvalue , так что Т - int ,
следовательно, тип param - iпt & &

В разделе 5.2 поясняется, почему эти примеры работают именно так, а не иначе. Клю­
чевым моментом является то, что правила вывода типов для параметров, являющихся
универсальными ссылками, отличаются от таковых для параметров, являющихся lvalue­
или rvаluе-ссылками. В частности, когда используются универсальные ссылки, вывод
типов различает аргументы, являющиеся lvalue, и аргументы, являющиеся rvalue. Этого
никогда не происходит для неуниверсальных ссылок.

Случай 3. Param�e не я вляется ни указателем, ни ссылкой
Когда Pa rarnType не является ни указателем, ни ссылкой, мы имеем дело с передачей
по значению:
ternplate
void f (T pararn) ;
// param передается по значению

Это означает, что pararn будет копией переданного функции - совершенно новым
объектом. Тот факт, что pararn будет совершенно новым объектом, приводит к правилам,
которые регулируют вывод Т из expr.
l.

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

2. Если после отбрасывания ссылочной части expr является const, это также иг­
норируется. Игнорируется и модификатор volat i l e (объекты vo l at i le являют­

ся редкостью и в общем случае используются только при реализации драйверов
устройств; детальную информацию на эту тему вы найдете в разделе 7.6.)
Таким образом, получаем следующее:
int х = 2 7 ;
const int сх = х ;
const int& rx = х;
f (х) ;
f ( сх ) ;
f ( rx ) ;

11
11
11
11
11
11

Как и ранее
Как и ранее
Как и ранее
Типами и Т , и pararn являются int
Типами и Т , и pararn внов ь являются iпt
Типами и Т , и pararn опять являются iпt

Обратите внимание, что даже несмотря на то, что сх и rx представляют константные
значения, pa rarn не является coпst. Это имеет смысл. pararn представляет собой объект,
который полностью независим от сх и rx,
это копия сх или rx. Тот факт, что сх и rx
-

1 . 1 . Вывод типа шаблона

27

не могут быть модифицированы, ничего не говорит о том, может ли быть модифициро­
ван pararn. Вот почему константность expr (как и volat i le, если таковой модификатор
присутствует) игнорируется при выводе типа param: то, что expr не может быть модифи­
цировано, не означает, что таковой должна быть и его копия.
Важно понимать, что const (и volat i le) игнорируются только параметрами, переда­
ваемыми по значению. Как мы уже видели, для параметров, которые являются ссылками
или указателями на const, константность expr при выводе типа сохраняется. Но рас­
смотрим случай, когда expr представляет собой соnst -указатель на константный объект,
а передача осуществляется по значению:
template
void f (Т param) ;

11 param передается по значению

11 ptr - константный указатель на
const char* const ptr
" Fun with pointers" ; // константный объект
=

f (ptr) ;

11 Передача arg типа const char* const

Здесь const справа от звездочки объявляет ptr константным: pt r не может ни указывать
на другое место в памяти, ни быть обнуленным. (const слева от звездочки гласит, что
ptr указывает на то, что (строка символов) является const, а следовательно, не может
быть изменено.) Когда pt r передается в функцию f, биты, составляющие указатель, копи­
руются в param. Как таковой сам указатель (ptr) будет передан по значению. В соответ­
ствии с правилом вывода типа при передаче параметров по значению константность pt r
будет проигнорирована, а выведенным для param типом будет const char*, т.е. изменя­
емый указатель на константную строку символов. Константность того, на что указывает
ptr, в процессе вывода типа сохраняется, но константность самого ptr игнорируется при
создании нового указателя param.

Ар гументы - масси вы
Мы рассмотрели большую часть материала, посвященного выводу типов шаблонов,
но есть еще один угол, в который стоит заглянуть. Это отличие типов массивов от ти­
пов указателей, несмотря на то что зачастую они выглядят взаимозаменяемыми. Основ­
ной вклад в эту иллюзию вносит то, что во множестве контекстов массив преобразуется
в указатель на его первый элемент. Это преобразование позволяет компилироваться коду
наподобие следующего:
const char name [ ] = "Briggs" ;
const char * ptrToName = name ;

// Тип name - const char [ l З J
/ / Массив становится указателем

Здесь указатель pt rToName типа const char* инициализируется переменной name, кото­
рая имеет тип const char ( 1 3 ] . Эти типы (const cha r* и const char [ 1 3 ] ) не являются
одним и тем же типом, но благодаря правилу преобразования массива в указатель при­
веденный выше код компилируется.
Но что будет, если передать массив шаблону, принимающему параметр по значению?
28

Глава 1 . Вывод типов

template
void f ( T param) ; / / Шаблон , получающий параметр по значению
f (пame ) ;

// Какой тип Т и param будет выведен?

Начнем с наблюдения, что не существует такой вещи, как параметр функции, являю­
щийся массивом. Да, да - приведенный далее синтаксис корректен:
void myFuпc ( iпt param [ ] ) ;

Однако объявление массива рассматривается как объявление указателя, а это означает,
что функция myFuпc может быть эквивалентно объявлена как
void myFuпc ( iпt* param) ; // Та же функция , что и ранее

Эта эквивалентность параметров, представляющих собой массив и указатель, образно го­
воря, представляет собой немного листвы от корней С на дереве С++ и способствует воз­
никновению иллюзии, что типы массивов и указателей представляют собой одно и то же.
Поскольку объявление параметра-массива рассматривается так, как если бы это было
объявление параметра-указателя, тип массива, передаваемого в шаблонную функцию
по значению, выводится как тип указателя. Это означает, что в вызове шаблонной функ­
ции f ее параметр типа Т выводится как const cha r * :
f (пame ) ; // паmе - массив, н о Т - coпst char*

А вот теперь начинаются настоящие хитрости. Хотя функции не могут объявлять пара­

метры как истинные массивы, они могут объявлять параметры, являющиеся ссьтками
на массивы! Так что если мы изменим шаблон f так, чтобы он получал свой аргумент
по ссылке,
template
void f ( T& param) ; / / Шаблон с передачей параметра по ссылке

и передадим ему массив
f (пame ) ;

/ / Передача массива функции f

то тип, выведенный для Т, будет в действительности типом массива! Этот тип включает
размер массива, так что в нашем примере т выводится как const char [ l 3 ] , а типом
параметра f (ссылки на этот массив) является const char ( & ) [ 1 3 ] . Да, выглядит этот
синтаксис как наркотический бред, но знание его прибавит вам веса в глазах понимаю­
щих людей.
Интересно, что возможность объявлять ссылки на массивы позволяет создать шаб­
лон, который выводит количество элементов, содержащихся в массиве:
// Возвращает размер массива как константу времени компиляции .
// Параметр не имеет имени, поскольку, кроме количества
// содержащихся в нем элементов, нас ничто не интересует .
template
coпstexpr std: : size t arraySize ( T (&) [N] ) поехсерt

1.1.

Вывод типа шаб л она

29

returп N ;

Как поясняется в разделе 3.9, объявление этой функции как coпstexpr делает ее ре­
зультат доступным во время компиляции. Это позволяет объявить, например, массив
с таким же количеством элементов, как и у второго массива, размер которого вычисляет­
ся из инициализатора в фигурных скобках:
11 keyVa ls содержит 7 элементов :
iпt keyVa l s [ ] = { 1 , 3 , 7 , 9, 1 1 , 2 2 , 35 };
iпt rnappedVal s [arraySize (keyVals) ] ; // rnappedVal s

-

тоже

Конечно, как разработчик на современном С++ вы, естественно, предпочтете std: : array
встроенному массиву:
11 Размер mappedVal s равен 7
std : : array mappedVal s ;

Что касается объявления arrayS i ze как поехсерt, то это помогает компилятору генери­
ровать лучший код. Детальнее этот вопрос рассматривается в разделе 3.8.

Ар rументы-функции
Массивы - не единственные сущности в С++, которые могут превращаться в указа­
тели. Типы функций могут превращаться в указатели на функции, и все, что мы говорили
о выводе типов для массивов, применимо к выводу типов для функций и их преобразо­
ванию в указатели на функции. В результате получаем следующее:
void someFunc ( iпt, douЬl e ) ; / / s omeFuпc - функция;
11 ее тип - void ( iп t , douЬle)
template
11 В f l param передается по значению
void f1 ( Т param} ;
template
void f2 ( T & param) ;

11 В f2 param передается по ссылке

f1 ( s omeFuпc} ;

11 param выводится как указатель на
11 функцию; тип - void ( * ) ( iпt , douЬle)

f2 ( s omeFuпc) ;

11 param выводится как ссыпка на
11 функцию; тип - void ( & ) ( iпt, douЬle )

Это редко приводит к каким-то отличиям н а практике, но если в ы знаете о преобразо­
вании массивов в указатели, то разберетесь и в преобразовании функций в указатели.
Итак, у нас есть правила для вывода типов шаблонов, связанные с auto. В нача­
ле я заметил, что они достаточно просты, и по большей части так оно и есть. Немно­
го усложняет жизнь отдельное рассмотрение согласованных lvalue при выводе типов
30

Глава 1 . Вывод типов

для универсальных ссылок, да еще несколько "мутят воду" правила преобразования
в указатели для массивов и функций. Иногда так и хочется, разозлившись, схватить ком­
пилятор и вытрясти из него "А скажи-ка, любезный, какой же тип ты выводишь?" Ког­
да это произойдет, обратитесь к разделу 1 .4, поскольку он посвящен тому, как уговорить
компилятор это сделать.
-

Сл едует запомнить


В процессе вывода типа шаблона аргументы, являющиеся ссылками, рассматрива­
ются как ссылками не являющиеся, т.е. их "ссылочность" игнорируется.



При выводе типов для параметров, являющихся универсальными ссылками, lvalue­
apryмeнты рассматриваются специальным образом.



При выводе типов для параметров, передаваемых по значению, аргументы, объяв­
ленные как const и/или volat i le, рассматриваются как не являющиеся ни const,
ни volat i le.



В процессе вывода типа шаблона аргументы, являющиеся именами массивов
или функций, преобразуются в указатели, если только они не использованы
для инициализации ссылок.

1 .2. Вывод типа auto
Если вы прочли раздел 1 . 1 о выводе типов шаблонов, вы знаете почти все, что следует
знать о выводе типа auto, поскольку за одним любопытным исключением вывод типа
auto представляет собой вывод типа шаблона. Но как это может быть? Вывод типа ша­
блона работает с шаблонами, функциями и параметрами, а auto не имеет дела ни с одной
из этих сущностей.
Да, это так, но это не имеет значения. Существует прямая взаимосвязь между выво­
дом типа шаблона и выводом типа auto. Существует буквальное алгоритмическое преоб­
разование одного в другой.
В разделе 1 . 1 вывод типа шаблона пояснялся с использованием обобщенного шаблона
функции
template
void f (Paraш7YPe param) ;

и обобщенного вызова
f ( expr) ; // Вызов f с некоторым выражением

При вызове f компиляторы используют expr для вывода типов т и ParamType.
Когда переменная объявлена с использованием ключевого слова auto, оно играет
роль Т в шаблоне, а спецификатор типа переменной действует как ParamType. Это проще
показать, чем описать, так что рассмотрим следующий пример:
auto

х =

27 ;

1 .2 .

Вывод типа auto

31

Здесь спецификатором типа для х является auto само по себе. С другой стороны, в объ­
явлении
const auto сх

=

х;

спецификатором типа является const auto. А в объявлении
const auto& rx

=

х;

спецификатором типа является const auto & . Для вывода типов для х, сх и rx в при­
веденных примерах компилятор действует так, как если бы для каждого объявления
имелся шаблон, а также вызов этого шаблона с соответствующим инициализирующим
выражением:
template
void func for х ( Т param) ;

1 1 Концептуальный шаблон для
11 вывода типа х

func for_x ( 2 7 ) ;

1 1 Концептуальный вызо в : выве1 1 денный тип param является
11 типом х

template
void func_for_cx ( const Т param) ;

1 1 Концептуальный шаблон для
11 вывода типа сх

func for_cx ( x ) ;

11 Концептуальный вызо в : выве11 денный тип param является
1 1ТИПОМ СХ

-

-

11 Концептуальный шаблон для
template
11
void func_for_rx ( const Т& param ) ;
вывода типа rx
11 Концептуальный вызов : выве11 денный тип param является
11 типом rx

func for_rx ( x ) ;
-

Как я уже говорил, вывод типов для auto представляет собой (с одним исключением,
которое мы вскоре рассмотрим) то же самое, что и вывод типов для шаблонов.
В разделе l . l вывод типов шаблонов был разделен на три случая, основанных на ха­
рактеристиках ParamType, спецификаторе типа param в обобщенном шаблоне функции.
В объявлении переменной с использованием auto спецификатор типа занимает место
ParamType, так что у нас опять имеются три случая.


Случай l . Спецификатор типа представляет собой ссылку или указатель, но не
универсальную ссылку.



Случай 2. Спецификатор типа представляет собой универсальную ссылку.



Случай 3. Спецификатор типа не является ни ссылкой, ни указателем.

Мы уже встречались со случаями
32

Гnава 1 . В ывод типов

l

и 3:

auto
х = 2 7 ; / / Случай 3 ( х не указатель и не ссЬUiка )
conзt auto сх
х; / / Случай 3 (сх не указатель и не ссыпка )
conзt auto& rx
х; / / Случай 1 ( rx - неуниверсальная ссылка )

Случай 2 работает, как и ожидалось:
auto&& urefl
auto&& uref2
auto&& ure f3

х; 11 х - int и lva lue, так что тип urefl - int&
сх ; 11 сх - const int и lvalue , так что тип
1 1 ure f2 - const int &
2 7 ; 1 1 2 7 - int и rvalue, так что тип
1 1 uref 3 - int & &

Раздел 1.1 завершился обсуждением того, как имена массивов и функций превраща­
ются в указатели для спецификаторов типа, не являющихся ссылками. То же самое происходит и при выводе типа auto:
const char name [ ]
" R . N . Briggs " ;
name ;
auto arrl
auto& arr2 = name ;
=

11 Тип name - const char [ 1 3 ]
11 Тип arrl
11 Тип arr2

const char*
const char ( &) [ 1 3 ]

void someFunc ( int , douЬle ) ; 11 someFunc - функция, ее тип
11 void ( int , douЬl e )
11
someFunc;
Тип funcl - void ( * ) ( int, douЬle)
auto funcl
11 Тип func2 - void ( & ) ( in t , douЬle)
someFunc;
auto& func2

Как можно видеть, вывод типа auto работает подобно выводу типа шаблона. По сути это
две стороны одной медали.
Они отличаются только в одном. Начнем с наблюдения, что если вы хотите объявить int
с начальным значением 27, С++98 предоставляет вам две синтаксические возможности:
int x l = 2 7 ;
int х2 ( 2 7 ) ;

С++ 1 1, поддерживая старые варианты инициализации, добавляет собственные:
int х3
{ 27 ) ;
int х4 { 2 7 } ;
=

Таким образом, у нас есть четыре разных синтаксиса, но результат один: переменная
типа int со значением 27.
Но, как поясняется в разделе 2 . 1 , объявление переменных с использованием ключево­
го слова auto вместо фиксированных типов обладает определенными преимуществами,
поэтому в приведенных выше объявлениях имеет смысл заменить int на auto. Простая
замена текста приводит к следующему коду:
auto
auto
auto
auto

xl = 2 7 ;
х2 ( 2 7 ) ;
{ 27 } ;
х3
х4 { 2 7 } ;
=

1 .2. Вывод типа auto

33

Все эти объявления компилируются, но их смысл оказывается не тем же, что и у объяв­
лений, которые они заменяют. Первые две инструкции в действительности объявляют
переменную типа int со значением 27. Вторые две, однако, определяют переменную типа
s t d : : i n i t ia l i zer l i st , содержащую единственный элемент со значением 27!
_

auto
auto
auto
auto

xl
27;
х2 ( 2 7 ) ;
хЗ = { 2 7 } ;
х4 { 2 7 } ;
=

11
11
11
11

Тип int , значение
27
То же самое
std : : initiali zer_list, значение ( 2 7 }
То же самое
-

Это объясняется специальным правилом вывода типа для auto. Когда инициализатор
для переменной, объявленной как a u t o , заключен в фигурные скобки, выведенный
тип - std : : ini t ial i zer_ 1 ist. Если такой тип не может быть выведен (например, из-за
того, что значения в фигурных скобках относятся к разным типам), код будет отвергнут:
auto х5 = { 1 , 2 , 3 . 0 } ; / / Ошибка ! Невозможно вывести Т
// для std: : initializer_l ist

Как указано в комментарии, в этом случае вывод типа будет неудачным, но важ­
но понимать, что на самом деле здесь имеют место два вывода типа. Один из них вы­
текает из применения ключевого слова auto: тип х5 должен быть выведен. Поскольку
инициализатор х 5 находится в фигурных скобках, тип х 5 должен быть выведен как
s t d : : i n i t i a l i z e r_l i s t . Но s t d : : i n i t i a l i z e r_l i s t - это шаблон. Конкретизация
представляет собой создание st d : : i n i t i a l i z e r_ l i s t с некоторым типом Т, а это
означает, что тип Т также должен быть выведен. Такой вывод относится ко второй раз­
новидности вывода типов - выводу типа шаблона. В данном примере этот второй вывод
неудачен, поскольку значения в фигурных скобках не относятся к одному и тому же типу.
Рассмотрение инициализаторов в фигурных скобках является единственным отличи­
ем вывода типа auto от вывода типа шаблона. Когда объявленная с использованием клю­
чевого слова auto переменная инициализируется с помощью инициализатора в фигурных
скобках, выведенный тип представляет собой конкретизацию s t d : : ini t i a l i z er_ l i s t .
Но если тот же инициализатор передается шаблону, вывод типа оказывается неудачным,
и код отвергается:
auto х

=

{ 11 , 23 , 9 } ; / / Тип х

-

std : : initializer list

templa te
void f (Т param) ;

// Объявление шаблона с параметром
// эквивалентно объявлению х

f ( { 11 , 23

// Ошибка вывода типа для Т

1

9 }) ;

Однако, если вы укажете в шаблоне, что раrаm представляет собой std: : i ni t i a l i zer_ list
для некоторого неизвестного т, вывод типа шаблона сможет определить, чем является Т:
template
void f ( std: : initializer_list initList ) ;

34

Глава 1 . Вывод типов

f ( { 11 , 23 , 9 } ) ; / / Вывод int в качестве типа Т, а тип
1 1 initList - std : : ini tiali zer l i s t

Таким образом, единственное реальное различие между выводом типа auto и выводом
типа шаблона заключается в том, что auto предполагает, что инициализатор в фигурных
скобках представляет собой std : : ini t i a l i zer_l i st, в то время как вывод типа шаблона
этого не делает.
Вы можете удивиться, почему вывод типа auto имеет специальное правило для ини­
циализаторов в фигурных скобках, в то время как вывод типа шаблона такого прави­
ла не имеет. Но я и сам удивлен. Увы, я не в состоянии найти убедительное объясне­
ние. Но "закон есть закон", и это означает, что вы должны помнить, что если вы объяв­
ляете переменную с использованием ключевого слова a u t o и инициализируете ее
с помощью инициализатора в фигурных скобках, то выводимым типом всегда будет
std : : i n i t i a l i z e r_l i st . Особенно важно иметь это в виду, если вы приверженец фи­
лософии унифицированной инициализации - заключения и нициализирующих значе­
ний в фигурные скобки как само собой разумеющегося стиля. Классической ошибкой
в С++ 1 1 является случайное объявление переменной std : : i n i t i a l i ze r_ l i s t там, где вы
намеревались объявить нечто иное. Эта ловушка является одной из причин, по которым
некоторые разработчики используют фигурные скобки в инициализаторах только тог­
да, когда обязаны это делать. (Когда именно вы обязаны так поступать, мы рассмотрим
в разделе 3. 1 .)
Что касается С++ 1 1 , то на этом история заканчивается, но для С++ 14 это еще не ко­
нец. С++ 14 допускает применение auto для указания того, что возвращаемый тип функ­
ции должен быть выведен (см. раздел 1 .3), а кроме того, лямбда-выражения С++ 14 могут
использовать auto в объявлениях параметров. Однако такое применение auto использу­
ет вывод типа шаблона, а не вывод типа auto. Таким образом, функция с возвращаемым
типом auto, которая возвращает инициализатор в фигурных скобках, компилироваться
не будет:
auto createinitLi s t ( )
return { 1 , 2 , 3 ) ; / / Ошибка : невозможно вывести
/ / ТИП ДЛЯ { 1 , 2, 3 )

То же самое справедливо и тогда, когда auto используется в спецификации типа парамет­
ра в лямбда-выражении С++ 14:
s td : : vector v ;

auto resetV =
[ &v] ( cons t auto& newValue ) { v

resetV ( { 1 , 2 , З } ) ;

newValue; ) ; 11 C++l4

/ / Ошибка : невозможно вывести
/ / ТИП ДЛЯ { 1 , 2, 3 )

1 .2.

Вывод типа auto

35

Сnедует запомнить


Вывод типа auto обычно такой же, как и вывод типа шаблона, но вывод типа auto,
в отличие от вывода типа шаблона, предполагает, что инициализатор в фигурных
скобках представляет s td : : i n i t i a l i ze r_ l i s t .



auto в возвращаемом типе функции или параметре лямбда-выражения влечет
применение вывода типа шаблона, а не вывода типа auto.

1 .3 . Знакомство с decl type
d e c l t ype
создание странное. Для данного имени или выражения dec l t ype сооб­
щает вам тип этого имени или выражения. Обычно то, что сообщает decl t ype,
это
именно то, что вы предсказываете. Однако иногда он дает результаты, которые заставля­
ют вас чесать в затылке и обращаться к справочникам или сайтам.
Мы начнем с типичных случаев, в которых нет никаких подводных камней. В отличие
от того, что происходит в процессе вывода типов для шаблонов и auto (см. разделы 1 . 1
и 1 .2), decl t ype обычно попугайничает, возвращая точный тип имени или выражения,
которое вы передаете ему:
-

-

const int i

=

О;

! / decltype ( i ) - const int

bool f ( const Widge t & w) ; / / decltype (w) - const Widget&
11 decltype ( f ) - bool ( const Widget & )
struct Point {
int х, у;

11

};

11

decltype ( Point : : x ) - int
decltype ( Point : : y ) - int

Widget w;

11

decltype (w) - Widget

if (f (w) ) ."

11

decltype ( f {w ) ) - bool

template
class vector {
puЫic :

11

Упрощенная версия std : : vector

Т& operator [ ] ( std : : si ze_t index ) ;
};

vector v ;
if (v[O]

==

0 ) ".

Видите� Никаких сюрпризов.

36

Глава 1 . В ывод типов

11

decltype (v) - vector

/ / decltype (v [ O ] ) - int&

Пожалуй, основное применение decl t уре в С++ 1 1
объявление шаблонов функций,
в которых возвращаемый тип функции зависит от типов ее параметров. Предположим,
например, что мы хотим написать функцию, получающую контейнер, который поддер­
живает индексацию с помощью квадратных скобок (т.е. с использованием " [ ] ") с индек­
сом, а затем аутентифицирует пользователя перед тем как вернуть результат операции
индексации. Возвращаемый тип функции должен быть тем же, что и тип, возвращаемый
операцией индексации.
ope r a t o r [ ] для контейнера объектов типа Т обычно возвращает Т & . Напри­
мер, это так в случае s t d : : deque и почти всегда - в случае s t d : : vect or. Однако
для std : : vect or оператор operator [ ] не возвращает boo l & . Вместо этого он воз­
вращает новый объект. Все "почему" и "как" данной ситуации рассматриваются в раз­
деле 2.2, но главное здесь то, что возвращаемый оператором ope rator [ ] контейнера тип
зависит от самого контейнера.
de cltype упрощает выражение этой зависимости. Вот пример, показывающий при­
менение decl t уре для вычисления возвращаемого типа. Этот шаблон требует уточнения,
но пока что мы его отложим.
-

template / / Работает, но
authAndAccess (Container& с , Index i )
1 1 требует

auto

- > decltype ( c [ i ] )

11

уточнения

authenticateUser ( J ;
return c ( i ] ;
Использование auto перед именем функции не имеет ничего общего с выводом типа. На
самом деле оно указывает, что использован синтаксис С++ 1 1
завершающий возвраща­
емый тип (trailing return type), т.е. что возвращаемый тип функции будет объявлен по­
сле списка параметров (после "->"). Завершающий возвращаемый тип обладает тем пре­
имуществом, что в спецификации возвращаемого типа могут использоваться параметры
функции. В authAndAccess, например, мы указываем возвращаемый тип с использовани­
ем с и i. Если бы возвращаемый тип, как обычно, предшествовал имени функции, с и i
были бы в нем недоступны, поскольку в этот момент они еще не были объявлены.
При таком объявлении aut hAndAc c e s s возвращает тот тип, который возвращает
ope rator [ ] при применении к переданному контейнеру, в точности как мы и хотели.
С++ 1 1 разрешает вывод возвращаемых типов лямбда-выражений из одной инструк­
ции, а С++ 1 4 расширяет эту возможность на все лямбда-выражения и все функции,
включая состоящие из множества инструкций. В случае authAndAcce s s это означает, что
в С++ 1 4 мы можем опустить завершающий возвращаемый тип, оставляя только одно
ведущее ключевое слово auto. При таком объявлении auto означает, что имеет место
вывод типа. В частности, это означает, что компиляторы будут выводить возвращаемый
тип функции из ее реализации:
-

template // С++ 1 4 ;
auto authAndAccess ( Container& с , Index i )
1 1 Не совсем
1 .3. Знакомство с decltype

37

1 1 корректно
authenticateUser ( ) ;
return c [ i ] ;
/ / Возвращаемый тип выводится из c [ i ]

В разделе 1 .2 поясняется, что для функций с аutо-спецификацией возвращаемого типа
компиляторы применяют вывод типа шаблона. В данном случае это оказывается пробле­
матичным. Как уже говорилось, operator [ ] для большинства контейнеров с объектами
типа Т возвращает Т&, но в разделе 1 . 1 поясняется, что в процессе вывода типа шаблона
"ссылочность" инициализирующего выражения игнорируется. Рассмотрим, что это озна­
чает для следующего клиентского кода:
s t d : : deque d;
authAndAcces s (d, 5)

10;

/ / Аутентифицирует пользователя, воз­
// вращает d [ 5 ] , затем присваивает ему
1 1 значение 1 0 . Код не компилируется !

Здесь d [ 5 ] возвращает i n t & , но вывод возвращаемого типа auto для authAndAccess от­
брасывает ссылку, тем самым давая возвращаемый тип i n t . Этот int, будучи возвра­
щаемым значением функции, является rvalue, так что приведенный выше код пытается
присвоить этому rvalue типа int значение IO. Это запрещено в С++, так что данный код
не компилируется.
Чтобы заставить authAndAccess работать так, как мы хотим, нам надо использовать
для ее возвращаемого типа вывод типа decltype, т.е. указать, что authAndAccess должна
возвращать в точности тот же тип, что и выражение с [ i ] . Защитники С++, предвидя не­
обходимость использования в некоторых случаях правил вывода типа decl t уре, сделали это
возможным в С++ \ 4 с помощью спецификатора decltype ( auto ) . То, что изначально может
показаться противоречием (decltype и auto?), в действительности имеет смысл: auto указы­
вает, что тип должен быть выведен, а decltype говорит о том, что в процессе вывода следует
использовать правила decltype. Итак, можно записать authAndAccess следующим образом:
template / /
//
//
authAndAccess ( Contai ner& с, I ndex i )
//
authent icateUser ( ) ;
return c [ i ] ;

declt:ype (auto)

С++ 1 4 ; работает,
но все еще
требует
уточнения

Теперь authAndAccess действительно возвращает то же, что и с [ i ] . В частности, в распро­
страненном случае, когда с [ i ] возвращает Т & , authAndAccess также возвращает Т&, и в том
редком случае, когда с [ i ] возвращает объект, authAndAccess также возвращает объект.
Использование decltype ( auto) не ограничивается возвращаемыми типами функций.
Это также может быть удобно для объявления переменных, когда вы хотите применять
правила вывода типа decltype к инициализирующему выражению:

38

Гnава 1 . Вывод типов

Widget w;
const Widget& cw = w;
auto myWidgetl = cw;

11
11

declt:ype (auto) myWidget2

Вывод типа auto :
тип myWidgetl - Widget

cw; // Вывод типа decltype :
1 1 тип myWidget2 - coпst Widget &

Я знаю, что вас беспокоят два момента. Один из них - упомянутое выше, но пока не
описанное уточнение authAndAccess. Давайте, наконец-то, разберемся в этом вопросе.
Еще раз посмотрим на версию aut hAndAcce s s в С++ 1 4:
template
decltype ( auto) authAndAcces s ( Container& с, Index i ) ;

Контейнер передается как lvalue-ccылкa на неконстантный объект, поскольку возвращае­
мая ссылка на элемент контейнера позволяет клиенту модифицировать этот контейнер. Но
это означает, что этой функции невозможно передавать контейнеры, являющиеся rvalue.
rvalue невозможно связать с lvаluе-ссылками (если только они не являются lvаluе-ссылками
на константные объекты, что в данном случае очевидным образом не выполняется).
Надо сказать, что передача контейнера, являющегося rvalue, в aut hAndAcce s s явля­
ется крайним случаем. Такой rvаluе-контейнер, будучи временным объектом, обычно
уничтожается в конце инструкции, содержащей вызов authAndAcce ss, а это означает, что
ссылка на элемент в таком контейнере (то, что должна вернуть функция authAndAccess)
окажется "висячей" в конце создавшей ее инструкции. Тем не менее передача временного
объекта функции aut hAndAcces s может иметь смысл. Например, клиент может просто
хотеть сделать копию элемента во временном контейнере:
std : : deque< std : : string> makeStringDeque ( ) ; 11 Фабричная функция
/ / Делаем копию пятого элемента deque, возвращаемого
11 функцией makeStringDeque
auto s = authAndAcces s (makeStringDeque ( ) , 5 ) ;

Поддержка такого использования означает, что мы должны пересмотреть объявление
функции authAndAccess, которая должна принимать как lvalue, так и rvallle. Можно ис­
пользовать перегрузку (одна функция объявлена с параметром, представляющим собой
lvalue-ccылкy, а вторая - с параметром, представляющим собой rvalue-ccылкy), но тогда
нам придется поддерживать две функции. Избежать этого можно, если у нас будет функ­
ция authAndAccess, использующая ссылочный параметр, который может быть связан как
с lvalue, так и с rvalue, и в разделе 5.2 поясняется, что это именно то, что делают универсаль­
ные ссылки. Таким образом, authAndAccess может быть объявлена следующим образом:
template / / Теперь с decltype ( auto) authAndAccess ( Container&& с, / / универсальная
Index i ) ;
/ / ссылка

1 .3. Знакомство с decltype

39

В этом шаблоне мы не знаем, с каким типом контейнера работаем, и точно так же не
знаем тип используемых им индексных объектов. Использование передачи по значению
для объектов неизвестного типа обычно сопровождается риском снижения произво­
дительности из-за ненужного копирования, проблемами со срезкой объектов (см. раз­
дел 8.1) и насмешками коллег. Но в случае индексов контейнеров, следуя примеру стан­
дартной библиотеки для значений индексов (например, в operator [ ] для s t d : : st ring,
std : : vector и s t d : : de que) это решение представляется разумным, так что мы будем
придерживаться для них передачи по значению.
Однако нам нужно обновить реализацию шаблона для приведения его в соответствие
с предостережениями из раздела 5.3 о применении std : : forward к универсальным ссылкам:
template / / Окончательная
decltype (auto)
/ / версия для
/ / С++14
authAnd.Acces s ( Container&& с , Index i )
authenticateUser ( ) ;
return std: : forward (c) [ i ) ;

Этот код должен делать все, что мы хотели, но он требует компилятора С++ 14. Если у вас
нет такового, вам следует использовать версию шаблона для С++ 1 1 . Она такая же, как
и ее аналог С++ 1 4, за исключением того, что вы должны самостоятельно указать воз­
вращаемый тип:
template / / Окончательная
/ / версия для
auto
authAnd.Acces s ( Container&& с, Index i )
/ / C++ l l
-> decl type ( std : : forward (с) [ i ] )
(

authenticateUser ( ) ;
return std : : forward ( с ) [ i ) ;

Вторым беспокоящим моментом является мое замечание в начале этого раздела о том,
что decl t уре почти всегда дает тип, который вы ожидаете, т.е. что он редко преподносит
сюрпризы. По правде говоря, вряд ли вы столкнетесь с этими исключениями из правила,
если только вы не занимаетесь круглосуточно написанием библиотек.
Чтобы полностью понимать поведение dec l t ype, вы должны познакомиться с неко­
торыми особыми случаями. Большинство из них слишком невразумительны, чтобы быть
размещенными в этой книге, но один из них приводит к лучшему пониманию decl type
и его применения.
Применение de c l t ype к имени дает объявленный тип для этого имени. Имена пред­
ставляют собой lvаluе-выражения, но это не влияет на поведение decl t ype. Однако
для lvаluе-выражений, более сложных, чем имена, decl t ype гарантирует, что возвраща­
емый тип всегда будет lvаluе-ссылкой. Иначе говоря, если lvаluе-выражение, отличное
от имени, имеет тип Т, то decl type сообщает об этом типе как об Т&. Это редко на что-то
40

Гл ава 1 . Вывод типов

влияет, поскольку тип большинства lvаluе-выражений в обязательном порядке включает
квалификатор lvаluе-ссылки. Например, функции, возвращающие lvalue, всегда возвра­
щают lvаluе-ссылки.
Однако у этого поведения есть следствия, о которых необходимо знать. В коде
int х

=

О;

является именем переменной, так что dec l t ype ( х ) представляет собой i n t . Однако
"заворачивание" имени х в скобки - " ( х ) " - дает выражение, более сложное, чем имя.
Будучи именем, х представляет собой lvalue, и С++ также определяет выражение ( х ) как
lvalue. Следовательно, declt ype ( ( х ) ) представляет собой i n t & . Добавление скобок во­
круг имени может изменить тип, возвращаемы й для него decl t ype !
В C++ l l это просто любопытный факт, но в сочетании с поддержкой в С++ 14
dec l t ype ( auto) это означает, что, казалось бы, тривиальные изменения в способе запи­
си инструкции return могут повлиять на выводимый тип функции:

х

decltype (auto)

f1 ( )

{
int х

=

О;

return х;

decl type (auto)

11 decltype ( x ) представляет собой int ,
// так что fl возвращает int

f2 ( )

{
int х

=

О;

return (х);

/ / decltype ( ( x) ) представляет собой int & ,
/ / так что f2 возвращает int&

Обратите внимание, что f2 не только имеет возвращаемый тип, отличный от fl, но
и возвращает ссылку на локальную переменную! Этот код ведет вас к неопределенному
поведению, что вряд ли является вашей целью.
Основной урок состоит в том, чтобы при использовании dec l t ype ( a u t o ) уделять
деталям самое пристальное внимание. Кажущиеся совершенно незначительными дета­
ли в выражении, для которого выводится тип, могут существенно повлиять на тип, воз­
вращаемый dec l t ype ( auto ) . Чтобы гарантировать, что выводимый тип - именно тот,
который вы ожидаете, используйте методы, описанные в разделе 1 .4.
В то же время не забывайте и о более широкой перспективе. Конечно, declt ype (как
автономный, так и в сочетании с auto ) при выводе типов иногда может привести к сюр­
призам, но это не нормальная ситуация. Как правило, dec l t ype возвращает тот тип, ко­
торый вы ожидаете. Это особенно верно, когда dec l t ype применяется к именам, потому
что в этом случае dec l t ype делает именно то, что скрывается в его названии: сообщает
объявленный тип (declared type) имени.

1 .3. Знакомство с decltype

41

Следует запомнить


dec l t ype почти всегда дает тип переменной или выражения без каких-либо из­

менений.


Для lvаluе-выражений типа т, отличных от имени, declt ype всегда дает тип Т &.



C++ l 4 поддерживает конструкцию decltype ( auto ) , которая, подобно auto, выво­
дит тип из его инициализатора, но выполняет вывод типа с использованием пра­
вил dec l t ype.

1 .4. Как просмотреть выведенные типы
Выбор инструментов для просмотра результатов вывода типа зависит от фазы про­
цесса разработки программного обеспечения, на которой вы хотите получить эту инфор­
мацию. Мы рассмотрим три возможности: получение информации о выводе типа при
редактировании кода, во время компиляции и во время выполнения.

Редакторы IDE
Редакторы исходных текстов в IDE часто показывают типы программных сущностей
(например, переменных, параметров, функций и т.п.), когда вы, например, помещаете
указатель мыши над ними. Например, пусть у вас есть код
const int theAnswer = 4 2 ;
theAnswer;
auto х
auto у = &theAnswer;

Редактор, скорее всего, покажет, что выведенный тип х представляет собой int, а выве­
денный тип у - const int * .
Чтобы это сработало, ваш код должен быть в более-менее компилируемом состоянии,
поскольку такого рода информация поставляется среде разработки компилятором С++
(или как минимум его клиентской частью}, работающим в IDE. Если компилятор не в со­
стоянии получить достаточно информации о вашем коде, чтобы выполнить вывод типа,
вы не сможете увидеть выведенные типы.
Для простых типов наподобие int информация из IDE в общем случае вполне точна.
Однако, как вы вскоре увидите, когда приходится иметь дело с более сложными типами,
информация, выводимая IDE, может оказаться не особенно полезной.

Диаrностика комп илятора
Эффективный способ заставить компилятор показать выведенный тип - использо­
вать данный тип так, чтобы это привело к проблемам компиляции. Сообщение об ошиб­
ке практически обязательно будет содержать тип, который к ней привел.
Предположим, например, что мы хотели бы узнать типы, выведенные для х и у из
предыдущего примера. Сначала мы объявляем шаблон класса, но не определяем его. Чего­
то такого вполне хватит:
42

Глава 1 . Вывод типов

template
class TD;

/! Только объявление TD;

Попытки инстанцировать этот шаблон приведут к сообщению об ошибке, поскольку ин­
станцируемый шаблон отсутствует. Чтобы увидеть типы х и у, просто попробуйте ин­
станцировать TD с их типами:
TD хТуре ; / / Сообщение об ошибке будет
TD уТуре ; / / содержать типы х и у

Я использую имена переменных вида variaЫeNameType, чтобы проще найти интере­
сующую меня информацию в сообщении об ошибке. Мой компилятор для приведенного
выше кода сообщает, в частности, следующее (я выделил интересующую меня информа­
цию о типах):
error: aggregate ' TD хТуре ' has incomplete type and
cannot Ье def ined
error: aggregate ' TD уТуре ' has incomplete type
and cannot Ье defined

Другой компилятор выдает ту же информацию, но в несколько ином виде:
error: ' хТуре ' uses undefined class ' TD '
error : ' уТуре ' uses unde fined class ' TD '

Если н е учитывать разницу в оформлении, все протестированные мною компиляторы
при использовании этого метода генерировали сообщения об ошибках с интересующей
меня информацией о типах.

В ывод времени вып оп нения
Подход с использованием функции вывода для отображения сведений о типе может
быть использован только во время выполнения программы, зато он предоставляет пол­
ный контроль над форматированием вывода. Вопрос в том, чтобы создать подходящее
для вывода текстовое представление информации. "Без проблем, - скажете вы. - Нам
на помощь придут t yp e i d и s t d : : type_i nfo : : name': В наших поисках информации
о выведенных для х и у типах можно написать следующий код:
std: : cout « typeid(x) . name ( ) « ' \n ' ; / / Выведенные типы
std : : cout
_

class Widget (
puЫ i c :
Widget ( int i , bool Ь ) ;
Widget ( int i , douЫe d ) ;

11 Как и ранее
11 Как и ранее

Widget (std: : initializer list il) ; // Добавлен
_

};

то w2 и w4 будут созданы с помощью нового конструктора, несмотря на то что тип
элементов s t d : : i п i t i a l i z e r l i s t (в данном случае
long douЫe ) хуже соот­
ветствует обоим аргументам по сравнению с конструкторами, не принимающими
st d : : ini t i a l i ze r l is t ! Смотрите сами:
_
-

Widget wl ( l O , true) ; / / Использует круглые скобки и, как и
11 ранее, вызывает первый конструктор
Widget w2 { 1 0 , true } ; / / Использует фигурные скобки, но теперь

64

Глава 3 . Переход к современному С++

1 1 вызывает третий конструктор
11 (10 и

true преобразуются в long douЬle )

Widget wЗ ( 1 0 , 5 . О) ;

1 1 Использует круглые скобки и, как и

Widget w4 { 1 0 , 5 . 0 } ;

1 1 Использует фигурные скобки , НО теперь

1 1 ранее, вьGывает второй конструктор
1 1 вызывает третий конструктор
11 (10 и

5 . 0 преобразуются в long douЬl e )

Даже то, что в обычной ситуации представляет собой копирующий или перемещающий
конструктор, может быть перехвачено конструктором с s td : : i n i t i а l i zer_l i s t :
class Widget {
puЬli c :
Widget ( int i , bool Ь ) ;
/ / Как ранее
Widget ( int i , douЫe d) ;
/ / Как ранее
Widget ( s td : : init ializer_list i l ) ; / / Как ранее
operator float ( ) const;

1 1 Преобразование во

float

};

Widget w5 (w4 ) ;

1 1 Использует круглые скобки , вызывает
1 1 копирукщий конструктор

Widget wб{w4 } ;

1 1 Использует фигурные скобки, вызов
1 1 конструктора с

s td : : initiali zer_list
( w4 преобразуется в о floa t , а float
1 1 преобразуется в long douЬle )
11

Widget w7 ( std : : move (w4 ) ) ; 1 1 Использует круглые скобки, вызывает
1 1 перемещающий конструктор

Widget w8 { std: : move (w4 ) } ; 1 1 Использует фигурные скобки, ВЬGОВ
1 1 конструктора с std : : initiali zer_l ist
1 1 ( все, как для w б )

Определение компилятором соответствия фигурных и нициализаторов конструкто­
рам с std : : i п i t i a l i z e r_ l i s t настолько строгое, что доминирует даже тогда, когда кон­
структор с s t d : : in i t i a l i ze r_l i s t с наилучшим соответствием не может быть вызван,
например:
class Widget
puЬli c :
Widget ( int i , bool Ь ) ;
/ / Как ранее
Widget ( int i , douЬle d) ; / / Как ранее
Widget ( std : : initiali zer_l ist i l ) ; / / Теперь тип
1 1 элемента

-

bool

/ / Нет функций неявного преобразования

Widget w{lO , 5 . 0 } ; // Ошибка ! Требуется сужающее преобразование

3 . 1 . Разл ичие между 11 и О при создании объектов

65

Здесь компилятор игнорирует первые два конструктора (второй из которых в точности
соответствует обоим типам аргументов) и пытается вызвать конструктор, получающий
аргумент типа std : : init i a l i z er_ l i s t . Вызов этого конструктора требует преоб­
разования значений int (10) и douЬl e ( 5 . О ) в bool. Оба эти преобразования являют­
ся сужающими (bool не может в точности представить ни первое, ни второе значения),
а так как сужающие преобразования запрещены в фигурных инициализаторах, вызов
является некорректным, и код отвергается.
И только если нет н икакой возможности преобразовать типы аргументов в фи­
гурном инициализаторе в типы в s t d : : i n i t i a l i z e r_l i s t, компилятор возвращает­
ся к нормальному разрешению перегрузки. Например, если мы заменим конструктор
с st d : : i n i t i a l i z e r_ l i s t конструктором, принимающим st d : : i n i t i a l i z e r_
l i st, то кандидатами на вызов вновь станут конструкторы, не принимаю­
щие std : : i n it i a l i z e r_ l ist (поскольку нет никакого способа преобразовать int и bool
в std : : st r i ng:
class Widget
puЫic :
Widget ( int i , bool Ь ) ;
1 1 Как ранее
Widget ( int i , douЫe d ) ; 1 1 Как ранее
1 1 Теперь тип элементов std : : initializer list - std : : string :
Widget ( s td : : initializer_list i l ) ;
11

Нет функций неявного преобразования

};
Widget
Widget
Widget
Widget

wl ( 1 0 ,
w2 { 1 0 ,
wЗ ( 1 0 ,
w4 { 1 0 ,

t rue ) ;
true } ;
5 . 0) ;
5.0};

11

Круглые скобки, первый конструктор
Фигурные скобки, первый конструктор
1 1 Круглые скобки, второй конструктор
1 1 Фигурные скобки, второй конструктор
11

Это приводит нас к завершению изучения фигурных инициализаторов и пере­
грузки конструкторов, но есть еще один интересный предельный случай, который хо­
телось бы рассмотреть. Предположим, что вы используете пустые фигурные скобки
для создания объекта, который поддерживает конструктор по умолчанию и конструк­
тор с s t d : : i n i t i a l i z e r_l i s t . Что при этом будут означать пустые фигурные скоб­
ки? Если они означают "без аргументов", будет вызван конструктор по умолчанию, но
если они означают "пустой s t d : : i n i t i a l i z e r_l i s t ", то будет вызван конструктор
с std : : i n i t i a l i ze r l i st без элементов.
Правило заключается в том, что будет вызван конструктор по умолчанию. Пустые
фигурные скобки означают отсутствие аргументов, а не пустой std : : i n i t i a l i ze r_ l i st:
class Widget {
puЬl i c :
1 1 Конструктор по умолчанию :
Widget ( ) ;
66

Глава 3. Переход к современному С++

/ / Конструктор с s t d : : initiali zer_li s t
Widget ( std : : initiali zer_li st i l ) ;
11

Нет функций неявного преобразования

f;
Widget wl ;
// Вызов конструктора по умолчанию
Widget w 2 { } ; // Вызов конструктора по умолчанию
Widget wЗ ( ) ; // Трактуется как объявление функции '

Если вы хотите вызвать конструктор с пустым std : : init i a l i ze r_ l i st, то это мож­
но сделать, передавая пустые фигурные скобки в качестве аргумента конструктора в кру­
глых или фигурных скобках, окружающих передаваемые вами:
Widget w4 ( { } ) ; // Вызов конструктора с пустым
// std : : initiali zer l i s t
Widget w5 ( { } } ; / / То же самое

Сейчас, когда кажущиеся магическими правила фигурной инициализации, s t d : :
ini t i a l i z e r_ l i s t и перегрузки конструкторов переполняют ваш мозг, вы можете уди­
виться, какое большое количество информации влияет на повседневное программи­
рование. На самом деле даже больше, чем вы думаете, потому что одним из классов,
на которые все это оказывает непосредственное влияние, является s t d : : vecto r. Класс
std : : vector имеет конструктор без s t d : : i n i t i a l i ze r_l i s t , который позволяет вам
указать начальный размер контейнера и значение, присваиваемое каждому из его эле­
ментов; но при этом имеется также конструктор, принимающий std : : i n i t i a l i zer_ l i s t
и позволяющий указать начальные значения контейнера. Если вы создаете s td : : vector
числового типа (например, std : : vect o r < i nt > ) и передаете ему два аргумента, то при ис­
пользовании круглых и фигурных скобок вы получите совершенно разные результаты:
std : : vector vl ( l O , 2 0 ) ; / / Используется конструктор без
// std : : initiali zer l i s t : создает
11 std: : vector с 10 элементами ;
1 1 значение каждого равно 20
std : : vector v2 { 1 0 , 2 0 } ; / / Используется конструктор с
11 s td : : initiali zer l i s t : создает
1 1 std: : vector с 2 элементами со
11 значениями 10 и 2 0

Но давайте сделаем шаг назад от std : : vector, а также от деталей применения круглых
скобок, фигурных скобок и правил перегрузки конструкторов. Имеется два основных
вывода из этого обсуждения. Во-первых, как автор класса вы должны быть осведомлены
о том, что если ваш набор перегружаемых конструкторов включает один или несколько
конструкторов, использующих std : : i n i t i a l i zer_l i s t , то клиентский код с фигурной
инициализацией может рассматривать только перегрузки с s t d : : i n i t i a l i z e r_l i st .
В результате лучше проектировать конструкторы так, чтобы перегрузка не зависела
от того, используете вы круглые или фигурные скобки. Другими словами, вынесите уроки
3.1 . Различие между 11 и () при создании объектов

67

из того, что сейчас рассматривается как ошибка дизайна интерфейса класса std : : vector,
и проектируйте свои классы так, чтобы избегать подобных ошибок.
Следствием этого является то, что если у вас есть класс без конструктора s t d : :
i n i t i a l i ze r l i st и вы добавляете таковой, то клиентский код, использующий фигур­
ную инициализацию, может обнаружить, что вызовы, разрешавшиеся с использованием
конструкторов без s t d : : in i t i a l i ze r_l i st , теперь разрешаются в новые функции. Ко­
нечно, такое может случиться в любой момент при добавлении новой функции ко мно­
жеству перегруженных функций: вызов, который разрешался в одну из старых функций,
теперь может приводить к вызову новой. Разница в данном случае в том, что перегрузки
с std : : i n i t i a l i ze r_ l i s t не только конкурируют с другими перегрузками, но практиче­
ски полностью перекрывают для них возможность быть рассмотренными в качестве по­
тенциальных кандидатов. Поэтому такое добавление должно выполняться только после
тщательного обдумывания.
Второй урок заключается в том, что в качестве клиента класса вы должны тщательно вы­
бирать между круглыми и фигурными скобками при создании объектов. Большинство раз­
работчиков в конечном итоге выбирают один вид скобок как применяемый по умолчанию,
а другой - только при необходимости. Применение по умолчанию фигурных скобок привле­
кает их непревзойденным диапазоном применимости, запретом применения сужающих пре­
образований и их иммунитетом к особенностям синтаксического анализа. Такие люди пони­
мают, что в некоторых случаях (например, при создании вектора std : : vector с заданными
размером и начальным значением элемента) необходимо использовать круглые скобки. С
другой стороны, немало программистов используют в качестве выбора по умолчанию круг­
лые скобки. Они привлекательны своей согласованностью с синтаксическими традициями
С++98, тем, что позволяют избегать проблем с выводом aut o как std : : i n i t i a l i zer_ l i st,
и уверенностью, что вызовы при создании объектов не приведут к случайным вызовам кон­
структоров с std : : init i a l i zer_l i s t . Эти программисты признают, что иногда следует ис­
пользовать именно фигурные скобки (например, при создании контейнера с определенными
значениями). Нет определенного превалирующего мнения о том, какой подход лучше, поэто­
му могу посоветовать только выбрать один из них и постоянно ему следовать.
Если вы автор шаблона, противостояние в применении круглых и фигурных скобок
может быть особенно неприятным, потому что в общем случае невозможно сказать,
какие скобки должны использоваться. Предположим, например, что вы хотите создать
объект произвольного типа с произвольным количеством аргументов. Использование
шаблонов с переменным количеством параметров позволяет сделать это концептуально
достаточно просто:
// Тип создаваемого объекта
template / / Типы используемых аргументов
void doSomeWork ( Ts&& . . . params )
Создание локального объекта Т из params . . .

68

Глава 3. Переход к современному С++

Есть два способа превратить строку псевдокода в реальный код (см. в разделе 5.З инфор­
мацию о std : : forward):
Т localObject (std : : forward (params ) . . . ) ; / / Круглые скобки
Т localObj ect { std: : forward (params ) . . } ; / / Фигурные скобки
.

Рассмотрим следующий вызывающий код:
std: : vector v;
doSomeWork ( l O ,

20) ;

Если doSomeWo rk использует при создании объекта l o ca lObj ect круглые скобки,
в результате будет получен s t d : : vector с 10 элементами. Если же doSomeWork исполь­
зует фигурные скобки, то результатом будет s t d : : vector с двумя элементами. Какой
из этих вариантов корректен! Автор doSomeWork не может этого знать. Это может знать
только вызывающий код.
Это именно та проблема, которая встает перед функциями стандартной библиотеки
s t d : : make_uni que и std : : ma ke_shared (см . раздел 4.4). Эти функции решают проблему,
используя круглые скобки и документируя это решение как части своих интерфейсов'.
Следует запомнит ь


Фигурная инициализация является наиболее широко используемым синтаксисом
инициализации, предотвращающим сужающие преобразования и нечувствитель­
ным к особенностям синтаксического анализа С++.



В процессе разрешения перегрузки конструкторов фигурные инициализаторы со­
ответствуют параметрам std : : i n it i a l i ze r_l i s t , если это возможно, даже если
другие конструкторы обеспечивают лучшее соответствие.



Примером, в котором выбор между круглыми и фигурными скобками приводит к зна­
чительно отличающимся результатам, является создание std : : vесtоr
с двумя аргументами.



Выбор между круглыми и фигурными скобками для создания объектов внутри ша­
блонов может быть очень сложным.

3 .2. Предпочитайте nullptr значениям О и NULL
Дело вот в чем: литерал О представляет собой int, а не указатель. Если С++ встретит
в контексте, где может использоваться только указатель, он интерпретирует О как нуле­
вой указатель, но это - запасной выход. Фундаментальная стратегия С++ состоит в том,
что О это значение типа i nt, а не указатель.
С практической точки зрения то же самое относится и к NULL. В случае NULL имеет­
ся некоторая неопределенность в деталях, поскольку реализациям позволено придавать
О

-

1

Возможен и более гибкий дизайн, который позволяет вызывающим функциям опреденить, должны
ли использоваться круглые или фигурные скобки в функциях, генерируемых из шаблонов. Подроб­
ности можно найти в записи от 5 июня 20 1 3 года в Andrzej's С++ Ыоg, "Intuitive interface - Part г:
3 .2. Предпочитайте nullptr значениям О и NULL

69

NU11 целочисленный тип, отличный от i nt (например, long). Это не является распро­

страненной практикой, но в действительности не имеет значения, поскольку вопрос не
в точном типе NU11, а в том, что ни О, ни NU11 не имеют тип указателя.
В С++98 основным следствием этого факта было то, что перегрузка с использованием
типов указателей и целочисленных типов могла вести к сюрпризам. Передача О или NU11
таким перегрузкам никогда не приводила к вызову функции с указателем:
void f ( int ) ;
void f ( bool ) ;
void f ( void* ) ;

11

f (0) ;
f ( NULL) ;

1 1 Вызов f ( int) , не f (void* )
/ / Может не компилироваться, но обычно
/ / вызывает f ( int ) и никогда - f ( void* )

Три nерегрузки функции f

Неопределенность в отношении поведения f ( NU11) является отражением свободы, пре­
доставленной реализациям в отношении типа NU11. Если NU11 определен, например, как 0 1
(т.е. О как значение типа l ong), то вызов является неоднозначным, поскольку преобразо­
вания long в i nt, long в bool и 01 в void* рассматриваются как одинаково подходящие.
Интересно, что этот вызов является противоречием между видимым смыслом исходного
текста ("вызываем f с нулевым указателем NU11 ) и фактическим смыслом ("вызываем f
с некоторой разновидностью целых чисел - не указателем"). Это противоречащее инту­
иции поведение приводит к рекомендации программистам на С++98 избегать перегрузки
типов указателей и целочисленных типов. Эта рекомендация остается в силе и в С++ 1 1 , по­
скольку, несмотря на рекомендации данного раздела, некоторые разработчики, определен­
но, продолжат применять О и NU11, несмотря на то что nul lptr является лучшим выбором.
Преимущество nullptr заключается в том, что это значение не является значением
целочисленного типа. Честно говоря, он не имеет и типа указателя, но его можно рассмат­
ривать как указатель любого типа. Фактическим типом nullpt r является std : : nul lpt r_t ,
ну, а тип s t d : : nul lptr_t циклически определяется как тип значения nu l lpt r " . Тип
std : : nul lptr_ t неявно преобразуется во все типы обычных указателей, и именно это де­
лает nul lptr действующим как указатель всех типов.
Вызов перегруженной функции f с nullptr приводит к вызову перегрузки void* (т.е. пе­
регрузки с указателем), поскольку nul lpt r нельзя рассматривать как что-то целочисленное:
»

11

f ( nullptr ) ;

Вызов f (void* )

Использование nu llptr вместо О или NU11, таким образом, позволяет избежать сюр­
призов перегрузки, но это не единственное его преимущество. Оно позволяет также по­
высить ясность кода, в особенности при применении аutо-переменных. Предположим,
например, что у нас есть следующий исходный текст:
auto result
findRecord ( / * Аргументы * / ) ;
i f (result == 0 ) {
=

70

Глава 3. Переход к современному С++

Если вы случайно не знаете (или не можете быстро найти), какой тип возвращает
f indRecord, может быть неясно, имеет ли result тип указателя или целочисленный тип.
В конце концов, значение О (с которым сравнивается resu lt) может быть в обоих случа­
ях. С другой стороны, если вы увидите код
auto result
i f (result

=

=

findRecord ( /* Аргументы * / ) ;
nullptr) {

то здесь нет никакой неоднозначности: resul t должен иметь тип указателя.
Особенно ярко сияет nul l p t r, когда на сцене появляются шаблоны. Предположим,
что у вас есть несколько функций, которые должны вызываться только при блокировке
соответствующего мьютекса. Каждая функция получает указатель определенного вида:
/ / Вызывается только при
int fl ( std: : shared_ptr spw ) ;
douЫe f2 ( s td : : unique_ptr upw ) ; 1 1 блокировке соответ 1 1 ствующего мьютекса
bool f3 (Widget* pw) ;

Вызывающий код с передачей нулевых указателей может выглядеть следующим об­
разом:
std: : mutex flm, f2m, f3m;

// Мьютексы для f l , f2 и f3

// C++l l typedef ; см . раздел 3 . 3
us ing MuxGuard
std : : lock_guard ;
=

MuxGuard g ( f lm) ;
auto result
fl (O) ;
=

MuxGuard g ( f2m) ;
auto result
f2 (NULL ) ;
=

11

Блокировка мьютекса для f l
Передача О функции f l
1 1 Разблокирование мьютекса

11

11

Блокировка мьютекса для f 2
Передача NULL функции f 2
/ / Разблокирование мьютекса

11

MuxGuard g ( f3m) ;
1 1 Блокировка мьютекса для f 3
auto result
f 3 (nullptr) ; 1 1 Передача nullptr функции f 3
1 1 Разблокирование мьютекса
=

То, что в первых двух вызовах не был передан nul lpt r, грустно; тем не менее код рабо­
тает, а это чего-то да стоит. Однако повторяющиеся действия еще более грустны. Они
просто беспокоят. Во избежание дублирования такого вида и предназначаются шаблоны,
так что давайте превратим эти действия в шаблон.

3.2. Предпочитайте nullptr э начениям О и N U L L

71

template
auto lockAndCal l ( FuncType func,
MuxType& mutex,
PtrType ptr) - > decl type ( func (ptr) )
using MuxGuard

=

std : : lock_guard ;

MuxGuard g (mutex ) ;
return func (ptr) ;

Если возвращаемый тип этой функции ( auto . . . - > de c l t ype ( func ( p t r ) ) заставляет вас
чесатьзатылок, обратитесь к разделу 1 .3, в котором объясняется происходящее. Там вы
узнаете, что в С++ 14 возвращаемый тип можно свести к простому decl t ype ( auto ) :
template
decltype (auto ) lockAndCall ( FuncType func, // С++ 1 4
MuxType& mutex,
PtrType ptr )
using MuxGuard

=

std : : lock_guard;

MuxGuard g (mutex ) ;
return func (ptr) ;

Для данного шаблона
следующий вид:

l oc kAndCa l l

(любой из версий), вызывающий код может иметь

auto resultl

lockAndCall ( fl , flm, 0 ) ;

11 Ошибка !

auto resul t2

lockAndCal l ( f2 , f2m, NULL ) ;

11 Ошибка !

auto result З

lockAndCall ( fЗ , fЗm, nullptr) ; 11 ОК

Такой код можно написать, но, как показывают комментарии, в двух случаях из трех
этот код компилироваться не будет. В первом вызове проблема в том, что когда О переда­
ется в lockAndC a l l , происходит вывод соответствующего типа шаблона. Типом О являет­
ся, был и всегда будет int, как и тип параметра ptr в инстанцировании данного вызова
lockAndC a l l . К сожалению, это означает, что в вызов func в l ockAndCa l l передается int,
а этот тип несовместим с параметром std : : shared ptr, ожидаемым функцией
f l . Значение О, переданное в вызове lockAndC a l l, призвано представлять нулевой указа­
тель, но на самом деле передается заурядный int. Попытка передать этот int функции f l
как std : : shared_pt r представляет собой ошибку типа. Вызов l ockAndCa ll с О
72

Глава 3. Переход к современному С ++

оказывается неудачным, поскольку в шаблоне функции, которая требует аргумент типа
std : : shared_pt r, передается значение i nt .
Анализ вызова с переданным NULL по сути такой же. Когда в функцию lockAndCa l l
передается NULL, для параметра p t r выводится целочисленный тип, и происходит ошиб­
ка, когда целочисленный тип передается функции f 2 , которая ожидает аргумент типа
std : : unique pt r.

В противоположность первым двум вызовам вызов с nul lpt r никакими неприятно­
стями не отличается. Когда функции lockAndCa l l передается nul lptr, выведенным ти­
пом pt r является std : : nul lpt r t . При передаче ptr в функцию fЗ выполняется неявное
преобразование std : : nul lpt r_t в W i dget * , поскольку std : : nul lpt r_t неявно преобра­
зуется во все типы указателей.
Тот факт, что вывод типа шаблона приводит к "неверным" типам для О и NULL (т.е. к их
истинным типам, а не к представлению с их использованием нулевых указателей), является
наиболее убедительной причиной для использования nullptr вместо О или NULL, когда вы
хотите представить нулевой указатель. При применении nullptr шаблоны не представля­
ют собой никаких особых проблем. Вместе с тем фактом, что nullptr не приводят к не­
приятностям при разрешении перегрузки, которым подвержены О и NULL, все это приводит
к однозначному выводу - если вам нужен нулевой указатель, используйте nul lpt r, но не
О и не NULL.
Следует запомнить


Предпочитайте применение nu l lptr использованию О или NULL.



Избегайте перегрузок с использованием целочисленных типов и типов указателей.

3.3. Предпочитайте объявnение псевдонимов
применению typedef
Я уверен, что мы можем сойтись на том, что применение контейнеров STL - хорошая
идея, и я надеюсь, что раздел 4. 1 убедит вас, что хорошей идеей является применение
std : : unique _pt r, но думаю, что ни один из вас не увлечется многократным написанием
типов наподобие s t d : : un ique_pt r>.
Одна мысль о таких типах лично у меня вызывает все симптомы синдрома запястного
канала2•
Избежать такой медицинской трагедии несложно, достаточно использовать t ypedef:
_

typedef
std: : unique_pt r
UPtrMapSS ;
2

Синдром заш1стного канала - неврологическое :iабоневанис, проявляющееся длительной бопью
и онемением папьцев рук. Широко распространено представление, что длительная ежедневная
работа на комньютере, требующая постоянного испопьзования кпавиатуры, явпяется фактором
риска развития синдрома запястного канапа. Примеч. пер.
-

3 .3. Предпочитайте объявпение псевдонимов применению typedef

73

Но typedef слишком уж какой-то девяносто восьмой . . . Конечно, он работает и в С++ 1 1,
но стандарт С++ 1 1 предлагает еще и объявление псевдонима (alias declaration):
usinq UPtrМapSS =

s td : : unique_ptr;

С учетом того, что t ypedef и объявление псевдонима делают в точности одно и то же,
разумно задаться вопросом "А есть ли какое-то техническое основание для того, чтобы
предпочесть один способ другому?"
Да, есть, но перед тем как я его укажу, замечу, что многие программисты считают
объявление псевдонима более простым для восприятия при работе с типами, включаю­
щими указатели на функции:
11 FP является синонимом дпя указателя на функцию, принимающую
/ / int и const std : : string& и ничего не возвращающую
typeclef void ( *FP) ( int, const s td : : string& ) ;
1 1 То же самое , но как объявление псевдонима
using FP = void ( * ) ( int , const s t d : : string& ) ;

Конечно, ни одна из разновидностей не оказывается существенно проще другой,
а ряд программистов тратит немало времени для того, чтобы верно записать синонимы
для типов указателей на функции, так что пока что убедительных причин для предпочте­
ния объявления псевдонима пока что нет.
Однако убедительная причина все же существует, и называется она - шаблоны.
В частности, объявления псевдонимов могут быть шаблонизированы (и в этом случае
они называются шаблонами псевдонимов), в то время как t ypedef
нет. Это дает про­
граммистам на С++ 1 1 простой механизм для выражения того, что в С++98 можно было
выразить только хакерскими способами, с помощью t ypedef, вложенных в шаблонные
s t ruct. Рассмотрим, например, определение синонима для связанного списка, который
использует пользовательский распределитель памяти MyAl loc. В случае шаблонов псев­
донимов это просто, как семечки щелкать:
-

// MyAl locList является синонимом дпя s t d : : l i st :
tamplate
using МyAllocList

=

std : : list;
// Клиентский код

MyAllocList lw;

В случае t ypedef эти семечки приходится сначала долго растить:
11 MyAllocList : : type

-

синоним дпя std : : list :

teшplate
struct МyAllocList {
typedef std : : list type ;

};
MyAllocList : : type lw; / / Клиентский код

74

Глава 3. Пере код к современному С++

Все еще хуже. Если вы хотите использовать t ypede f в шаблоне для создания связан­
ного списка, хранящего объекты типа, указанного параметром шаблона, имя, указанное
в t ypedef, следует предварять ключевым словом t ypename:
template
11 Widget содержит
class Widget [
/ / MyAllocList,
private :
typename MyAllocList : : type l i s t ; / / как член-данные

);

Здесь MyAl locL i s t : : t ype ссылается на тип, который зависит от параметра типа
шаблона (Т). Тем самым MyAl locLi s t : : t ype является зависимь1м типом (depeпdent
type), а одно из многих милых правил С++ требует, чтобы имена зависимых типов пред­
варялись ключевым словом t ypename.
Если MyA l locLi s t определен как шаблон псевдонима, это требование использования
ключевого слова t ypename убирается (как и громоздкий суффикс " : : t ype "):
template
using MyAllocList
std : : l ist; // Как и ранее
=

template
class Widget {
private :
МyAllocList list ;

1 1 Ни typenarne ,
11 ни : : type

);

Для вас MyAl locL i s t (т.е. использование шаблона псевдонима) может выглядеть
как зависимый от параметра шаблона т, как и M yA l l ocLi s t : : t ype (т.е. как и ис­
пользование вложенного t ypede f), но вы не компилятор. Когда компилятор обраба­
тывает шаблон W idget и встречает использование MyA l l ocLi s t (т.е. использование
шаблона псевдонима), он знает, что MyA l locLi s t является именем типа, поскольку
MyAl locL i s t является шаблоном псевдонима: он о бязан быть именем типа. Тем самым
MyAl locList оказывается независимым типом, и спецификатор t ypename не является
ни требуемым, ни разрешенным.
С другой стороны, когда компилятор видит MyAl locL i s t : : t уре (т.е. использова­
ние вложенных t ypedef) в шаблоне Wi dget, он не может знать наверняка, что эта кон­
струкция именует тип, поскольку это может быть специализация MyAl l ocList, с которой
он еще не встречался и в которой MyAl l ocLi s t : : t ype ссылается на нечто, отличное
от типа. Это звучит глупо, но не вините компиляторы за то, что они рассматривают та­
кую возможность. В конце концов, это люди пишут такой код.
Например, некая заблудшая душа вполне в состоянии написать следующее:
class Wine {
ternplate

_

);
// Специализация MyAllocList в
3.3. Предпочитайте обьявnение псевдонимов применению typedef

75

11 которой Т представляет собой Wine
class MyAll ocList
private :
enшn class WineType
/ / См. в разделе 3 . 4 информацию об
{ White, Red, Rose } ; // "enшn class"
WineType type;

11 В этом классе type представляет
11 собой данные-член 1

};

Как видите, MyAl locL i s t : : t ype н е является типом. Если Widget инстанциро­
ван с W i ne, MyAl locLi s t : : t ype в шаблоне W idget представляет собой данные-член,
а не тип. Ссылается ли MyAl l ocLi st : : t ype на тип в шаблоне W idget, зависит от того,
чем является Т, а потому компиляторы требуют, чтобы вы точно указывали, что это тип,
предваряя его ключевым словом t ypename.
Если вы занимаетесь метапрограммированием с использованием шаблонов (template
metaprogramming ТМР), то вы, скорее всего, сталкивались с необходимостью получать
параметры типов шаблонов и создавать из них новые типы. Например, для некоторого
заданного типа т вы можете захотеть удалить квалификатор con s t или квалификатор
ссылки, содержащийся в Т, например преобразовать const std : : s t r i ng& в std : : s t r i ng.
Вы можете также захотеть добавить const к типу или преобразовать его в lvalue-ccылкy,
например, превращая W idget в const Widget или в Widge t &. (Если вы еще не занима­
лись ТМР, это плохо, потому что, если вы действительно хотите быть эффективным про­
граммистом на С++, вы должны быть знакомы как минимум с основами этого аспекта
С++. Вы можете увидеть примеры ТМР в действии, включая различные преобразования
типов, о которых я упоминал, в разделах 5 . 1 и 5.5.)
С++ 1 1 дает вам инструменты для такого рода преобразований в виде свойств ти­
пов (type traits), набора шаблонов в заголовочном файле < t ype_t r a i t s > . В нем вы
найдете десятки свойств типов; не все из них выполняют преобразования типов, но
те, которые это делают, предлагают предсказуемый интерфейс. Для заданного типа Т,
к которому вы хотели бы применить преобразование, результирующий тип имеет вид
s t d : : преобразование : : t ype, например:
-

std : : remove_const : : type
// Дает Т из const Т
std : : remove_reference : : type
// Дает Т из Т& и Т&&
std : : add_lvalue_reference : : type // Дает Т& из Т

Комментарии просто резюмируют, что делают эти преобразования, так что не прини­
майте их слишком буквально. Перед тем как использовать их в своем проекте, я настоя­
тельно советую ознакомиться с их точной спецификацией.
В любом случае я не стремлюсь обеспечить вас учебником по свойствам типов. Вмес­
то этого я прошу вас обратить внимание на то, что каждое преобразование завершается
: : t ype". Если вы применяете их к параметру типа в шаблоне (что практически всегда
является их применением в реальном коде), то вы также должны предварять каждое их
применение ключевым словом t ypename. Причина обоих этих синтаксических требова­
ний заключается в том, что свойства типов в C++l l реализованы как вложенные t ypedef
"

76

Гn ава 3. Переход к современному С++

внутри шаблонных структур struct. Да, это так - они реализованы с помощью техноло­
гии, о которой я говорю, что она уступает шаблонам псевдонимов!
Тому есть исторические причины, но здесь мы их опустим (честное слово, это слиш­
ком скучно), поскольку Комитет по стандартизации с опозданием признал, что шаблоны
псевдонимов оказываются лучшим способом реализации, и соответствующие шаблоны
включены в С++ \4 для всех преобразований типов C++ l l . Псевдонимы имеют общий
вид: для каждого преобразования C++ l l std : : преобразование : : t ype имеется соот­
ветствующий шаблон псевдонима С++ 14 с именем std : : преобразов ание_ t. Вот приме­
ры, поясняющие, что я имею в виду:
std:
std :
std :
std :
std :
std :

: remove_const : : type
: remove const t
: remove_reference : : type
: remove reference t
: add_lva lue_reference : : type
: add lvalue reference t

11 C++ l l : const Т -> Т
/ / Эквивалент в С++ 1 4
/ / C++l l : Т & / Т & & - > Т
11 Эквивалент в С++ 1 4
/ / C++l l : Т - > Т&
/ / Эквивалент в С++ 1 4

Конструкции С++ 1 1 остаются в силе в С++ 1 4, но я не знаю, зачем вам может захотеть­
ся их использовать. Даже если у вас нет компилятора С++ 14, написание таких шаблонов
псевдонимов самостоятельно - детская игра. Требуются только языковые возможности
С++ 1 1, и даже ребенок сможет написать такие шаблоны. Если у вас есть доступ к элек­
тронной копии стандарта С++ 14, то все становится еще проще - вы можете просто ско­
пировать необходимый код оттуда и вставить в свою программу. Вот вам для начала:
template
using remove_const t

typename remove_const : : type ;

template
using remove_reference t

typename remove_reference : : type ;

template
using add_lvalue_re ference t =
typename add_lvalue_reference : : type ;

Судите сами - что может быть проще?
Следует запомнить



В отличие от объявлений псевдонимов, typede f не поддерживает шаблонизацию.
Шаблоны псевдонимов не требуют суффикса : : t уре ·: а в шаблонах - префикса
"

t ypename, часто требуемого при обращении к t ypede f.


С++ 14 предлагает шаблоны псевдонимов для всех преобразований свойств типов
C++ l l .

3.3. Предпочитайте объявление псевдонимов применению typedef

77

3 .4. П редпочита йте перечисл ения с областью
видимости перечислениям без таковой
В качестве общего правила объявление имени в фигурных скобках ограничивает
видимость этого имени областью видимости, определяемой этими скобками. Но не так
обстоит дело с перечислениями в С++98. Имена в таких перечислениях принадлежат об­
ласти видимости, содержащей епшn, а это означает, что ничто иное в этой области види­
мости не должно иметь такое же имя:
enum Color {Ьlack, white, red} ; / / Ыасk, white , red находятся
// в той же области видимости,
11 что и Color
// Ошибка ! Имя white уже объяв­
auto white
false ;
// лено в этой области видимости

Тот факт, что эти имена перечисления "вытекаютn в область видимости, содержащую
определение их enшn, приводит к официальному термину для данной разновидности пе­
речислений: без о бласти в идимости ( unscoped) . Их новый аналог в С++ 1 1 , перечисления
с о бластью ви д имости ( scope d enum ) , не допускает такой утечки имен:
enUlll class Color

{ Ыасk, white, red } ; // Ыасk, white, red принадлежат
11 области видимости Color
auto white = false ;
// ОК, других white нет
Color с

=

white;

// Ошибка ' Нет имени перечисления
// "white" в этой области видимости

Color с

=

Color : : white;

11 ок

auto с = Color : : white;

11
11

ОК (и соответствует совету
из раздела 2 . 1 )

Поскольку eпum с областью видимости объявляются с помощью ключевого слова
class, о них иногда говорят как о классах перечислений.
Снижение загрязнения пространства имен, обеспечиваемое применением перечисле­
ний с областью видимости, само по себе является достаточной причиной для предпо­
чтения таких перечислений их аналогам без областей видимости. Однако перечисления
с областью видимости имеют и второе убедительное преимущество: они существенно
строже типизированы. Значения в перечислениях без областей видимости неявно пре­
образуются в целочисленные типы (а оттуда - в типы с плавающей точкой). Поэтому
вполне законными оказываются такие семантические карикатуры:
1 1 Перечисление без
1 1 области видимости
std: : vector
1 1 Функция, возвращающая
primeFactors ( s td : : size t х ) ; 1 1 простые делители х

eпum Color { Ьlack, white, red } ;

78

Гnава 3. Переход к современному С++

Color с

=

red;
11 Сравнение Color и douЫe ( ! )
11 Вычисление простых делителей
11 значения Color ( ! )

if (с < 1 4 . 5 ) {
auto factors
primeFactors ( c ) ;

Добавление простого ключевого слова c l a s s после enum преобразует перечисление
без области видимости в перечисление с областью видимости, и это - совсем другая
история. Не имеется никаких неявных преобразований элементов перечисления с облас­
тью видимости в любой другой тип:
/ / Перечисление с областью видимости
eпum class Color
[ Ьlack, white, red } ;
11
11
11
if (с < 1 4 . 5 ) {
11
11
auto factors
primeFactors ( c ) ; 11

Color с

=

Color : : red;

К а к и ранее, но с квалификатором
области ВИДИМОСТИ
Ошибка ! Нельзя сравнивать
Color и douЫe
Ошибка ! Нельзя передавать Color в
функцию, ожидающую std: : size t

Если вы хотите честно выполнить преобразование из Color в другой тип, сделайте то
же, что вы всегда делаете для осуществления своих грязных желаний, - воспользуйтесь
явным приведением типа:
if (static_cast (c) < 1 4 . 5 ) { / / Странный, но
11 корректный код
11 Сомнительно, но компилируется
auto factors
primeFactors ( static_cast ( c) ) ;
=

Может показаться, что перечисления с областями видимости имеют и третье преиму­
щество перед перечислениями без областей видимости, поскольку могут быть предвари­
тельно объявлены, их имена могут быть объявлены без указания перечислителей:
eпum Color;
eпum class Color;

11 Ошибка !
11 ок

Это заблуждение. В С++ 1 1 перечисления без областей видимости также могут быть
объявлены предварительно, но только с помощью небольшой дополнительной работы,
которая вытекает из того факта, что каждое перечисление в С++ имеет целочисленный
базовый тип (underlying type), который определяется компилятором. Для перечисления
без области видимости наподобие Color
eпum Color { Ы асk, white, red } ;
3.4. Предпочитайте перечисления с область ю видимости перечис лениям без таковой

79

компилятор может выбрать в качестве базового типа char, так как он должен предста­
вить всего лишь три значения. Однако некоторые перечисления имеют куда больший
диапазон значений, например:
enum Status { good
О,
failed = 1 ,
incomplete = 1 0 0 ,
corrupt
200,
indeterminate = OxFFFFFFFF
};
=

=

Здесь должны быть представлены значения в диапазоне от О до OxFFFFFFFF. За исключе­
нием необычных машин (где char состоит как минимум из 32 битов), компилятор выбе­
рет для предоставления значений Status целочисленный тип, больший, чем cha r.
Для эффективного использования памяти компиляторы часто выбирают наименьший
базовый тип, которого достаточно для представления значений перечислителей. В не­
которых случаях, когда компиляторы выполняют оптимизацию по скорости, а не по раз­
меру, они могут выбрать не наименьший допустимый тип, но при этом они, определенно,
захотят иметь возможность оптимизации размера. Для этого С++98 поддерживает толь­
ко определения e num (в которых перечислены все значения); объявления e num не раз­
решены. Это позволяет компиляторам выбирать базовый тип для каждого enum до его
использования.
Но невозможность предварительного объявления e num имеет свои недостатки. Наи­
более важным из них, вероятно, является увеличение зависимостей при компиляции.
Вновь обратимся к перечислению St atus:
enum Status { good = О ,
failed = 1 ,
incomplete
100,
corrupt = 2 0 0 ,
indeterminate = OxFFFFFFFF
};
=

Это разновидность перечисления, которая, скорее всего, будет использоваться во всей
системе, а следовательно, включенная в заголовочный файл, от которой зависит каждая
из частей системы. Если добавить новое значения состояния
enum Status { good
О,
fai led
1,
incomplete
100,
corrupt
200,
audi ted = 500 ,
indeterminate
OxFFFFFFFF
};
=

=

=

=

=

то, вероятно, придется перекомпилировать всю систему полностью, даже если только
одна подсистема - возможно, одна-единственная функция! - использует это новое

80

Гла ва З . Переход к современному С++

значение. Это одна из тех вещей, которые программисты просто ненавидят. И это та
вещь, которую исключает возможность предварительного объявления eпum в С++ 1 1 . На­
пример, вот совершенно корректное объявление eпum с областью видимости, и функции,
которая получает его в качестве параметра:
/ / Предварительное объявление
епшп class Status;
void coпtiпueProcess iпg ( Status s ) ; / / и его использование

Заголовочный файл, содержащий эти объявления, не требует перекомпиляции при пере­
смотре определения Status. Кроме того, если изменено перечисление Status (например,
добавлено значение audited) , но поведение coпt iпueProcessiпg не изменилось (напри­
мер, потому что coпt iпue Proces s i пg не использует значение audited) , то не требуется
и перекомпиляция реализации coпt iпueProcessiпg.
Но если компилятор должен знать размер eпum до использования, то как могут пере­
числения С++ 1 1 быть предварительно объявлены, в то время как перечисления С++98
этого не могут� Ответ прост: базовый тип перечислений с областью видимости всегда
известен, а для перечислений без областей видимости вы можете его указать.
По умолчанию базовым типом для eпum с областью видимости является iпt:
епшп class Status ; / / Базовый тип - iпt

Если вас не устраивает значение по умолчанию, вы можете его перекрыть:
епшп class Status : std : : uint32_t; / / Базовый тип для Status 11 std : : uint32 t (из )

В любом случае компиляторы знают размер перечислителей в перечислении с областью
видимости.
Чтобы указать базовый тип для перечисления без области видимости, вы делаете то
же, что и для перечисления с областью видимости, и полученный результат может быть
предварительно объявлен:
епшп Color : std: : uintB_t ; // Предварительное объявление
11 перечисления без области видимости ;
11 базовый тип - std : : uint8 t

Спецификация базового типа может быть указана и в определении eпum:
еnшп class Status : std: : uint32 t

good
О,
failed = 1 ,
incomplete = 1 00 ,
corrupt
200,
audited
500,
indeterminate
OxFFFFFFFF
=

=

=

=

};

С учетом того факта, что eпum с областью видимости устраняет загрязнение простран­
ства имен и невосприимчиво к бессмысленным преобразованиям типов, вас может уди­
вить тот факт, что имеется как минимум одна ситуация, в которой могут быть полезны

3 .4. Предпочитайте перечиспения с обnастью видимости перечисnениям без таковой

81

перечисления без области видимости, а именно - при обращении к полям в кортежах
C++ l l s t d : : tupl e . Предположим, например, что у нас есть кортеж, содержащий имя,
адрес электронной почты и значение репутации пользователя на сайте социальной сети:
using Userinfo =
std : : tuple

//
11
11
11

Псевдоним типа ; см. раздел 3 . 3
Имя
Адрес
Репутация

Хотя комментарии указывают, что представляет собой каждое поле кортежа, это,
вероятно, не слишком полезно, когда вы сталкиваетесь с кодом наподобие следующего
в отдельном исходном файле:
Userinfo uinfo ;
auto val

=

/ / Объект с типом кортежа

std : : get ( uinfo ) ; / / Получение значения поля 1

Как программисту вам приходится отслеживать множество вещей. Вы действительно
должны помнить, что поле 1 соответствует адресу электронной почты пользователя? Я
думаю, нет. Использование e nurn без области видимости для сопоставления имен полей
с их номерами позволяет избежать необходимости перегружать память:
enum

UserinfoFields { uiName , uiEmail , ui.Reputation } ;

1 1 Как и ранее

Userinfo uinfo;
auto val

=

std : : get (uinfo ) ; / / Значение адреса

Все было бы гораздо сложнее без неявного преобразования значений из Userin foFie lds
в тип st d : : s i ze_t , который является типом, требующимся для s t d : : get.
Соответствующий код с применением перечисления с областью видимости сущес­
твенно многословнее:
enum class UserinfoFields { uiName , uiEma i l , uiReputation } ;
Userinfo uinfo ;

11 Как и ранее

auto val
std: : get
(uinfo ) ;

Эта многословность может быть сокращена с помощью функции, которая принимает
перечислитель и возвращает соответствующее значение типа s t d : : s i ze_t , но это немно­
го сложнее. s t d : : g e t является шаблоном, так что предоставляемое значение является
аргументом шаблона (обратите внимание на применение не круглых, а угловых скобок),
так что функция, преобразующая перечислитель в значение s t d : : s i ze_t, должна давать
результат во время компиляции. Как поясняется в разделе 3.9, это означает, что нам нуж­
на функция, являющаяся constexpr.

82

Глава 3. Переход к современному С++

Фактически это должен быть соnstехрr-шаблон функции, поскольку он должен рабо­
тать с любыми перечислениями. И если мы собираемся делать такое обобщение, то долж­
ны обобщить также и возвращаемый тип. Вместо того чтобы возвращать s t d : : s i ze_t,
мы должны возвращать базовый тип перечисления. Он доступен с помощью свойства
типа s t d : : unde r l yi ng _ t ype (о свойствах типов рассказывается в разделе 3.3). Наконец
мы объявим его как noexcept (см. раздел 3.8), поскольку мы знаем, что он н икогда не
генерирует исключений. В результате мы получим шаблон функции t oUType, который
получает произвольный перечислитель и может возвращать значение как константу вре­
мени компиляции:
template
constexpr typename std : : underlying_type : : type
toUType ( E enumerator) noexcept
return
stat ic_cast ( enumerator) ;

В С++ 14 t oUType можно упростить заменой t ypename s t d : : unde r l y ing t ype : : t ype
более изящным s t d : : unde r l ying_t ype_t (см. раздел 3.3):
template
constexpr std : : underlying_type t
toUType ( E enumerator) noexcept

// С++14

return static_cast ( enumerato r ) ;

Еще более изящный возвращаемый тип auto (см. раздел 1 .3) также корректен в С++ 14:
template // С++ 1 4
constexpr auto
toUType ( E enumerator) noexcept
return static_cast ( enumerator ) ;

Независимо от того, как он написан, шаблон t oUType позволяет нам обратиться к полю
кортежа следующим образом:
auto val

=

std: : get (uinfo ) ;

Это все же больше, чем достаточно написать при использовании перечисления без об­
ласти видимости, но зато позволяет избежать загрязнения пространства имен и непред­
намеренных преобразований перечислителей. Во многих случаях вы можете решить, что
набор нескольких дополнительных символов является разумной ценой за возможность из­
бежать ловушек перечислений, появление которых восходит ко времени, когда вершиной
достижений в цифровых телекоммуникациях был модем со скоростью 2400 бод.
3.4. Предпочи тайте перечисления с облас т ью видимости перечислениям без таковой

83

Следует запомнить


Перечисления в стиле С++98 в настоящее время известны как перечисления без
областей В ИДИ МОСТИ.



Перечислители перечислений с областями видимости видимы только внутри пере­
числений. Они преобразуются в другие типы только с помощью явных приведений.



Как перечисления с областями видимости, так и без таковых поддерживают ука­
зание базового типа. Базовым типом по умолчанию для перечисления с областью
видимости является i nt . Перечисление без области видимости базового типа
по умолчанию не имеет.



Перечисления с областями видимости могут быть предварительно объявлены. Пе­
речисления без областей видимости могут быть предварительно объявлены, толь­
ко если их объявление указывает базовый тип.

3 . 5 . П редпо ч итайте удапенные функции
закрытым неопредепенным
Если вы предоставляете код другим разработчикам и хотите предотвратить вызов
ими некоторой функции, обычно вы просто ее не объявляете. Нет объявления функ­
ции - нечего и вызывать. Но иногда С++ объявляет функции вместо вас, и если вы хо­
тите предотвратить вызов таких функций клиентами вашего кода, придется постараться.
Эта ситуация возникает только для "специальных функций-членов", т.е. функций­
членов, которые при необходимости С++ генерирует автоматически. В разделе 3. 1 1 эти
функции рассматриваются более подробно, а пока что мы будем беспокоиться только
о копирующем конструкторе и копирующем операторе присваивания. Эта глава во мно­
гом посвящена распространенным практикам С++98, для которых есть более эффектив­
ная замена в С++ 1 1 , а в С++98, когда вы хотите подавить применение функции-члена,
это почти всегда копирующий конструктор, оператор присваивания или они оба.
Подход С++98 для предотвращения применения этих функций состоит в объявлении
их как private без предоставления определений. Например, вблизи с основанием иерар­
хии потоков ввода-вывода в стандартной библиотеке С++ находится шаблонный класс
ba s i c_ios. Все классы потоков наследуют (возможно, косвенно) этот класс. Копирование
потоков ввода-вывода нежелательно, поскольку не совсем очевидно, что же должна де­
лать такая операция. Объект i s t r earn, например, представляет поток входных значений,
одни из которых могут уже быть считаны, а другие могут потенциально быть считаны
позже. Если копировать такой поток, то должно ли это повлечь копирование всех счи­
танных значений, а также значений, которые будут считаны в будущем� Простейший
способ разобраться в таких вопросах - объявить их несуществующими. Именно это де­
лает запрет на копирование потоков.
Чтобы сделать классы потоков некопируемыми, bas i c_ios в С++98 объявлен следую­
щим образом (включая комментарии):
84

Гnава З . Переход к современному Ctt

template

private :

/ / Не определен
basic ios ( const basic_ios& ) ;
basic ios& operator= ( const basic_ios & ) ; / / Не определен
};

Объявление этих функций как pri vate предотвращает их вызов клиентами. Умыш­
ленное отсутствие их определений означает, что если код, все еще имеющий к ним до­
ступ (т.е. функции-члены или друзья класса), ими воспользуется, то компоновка (редак­
тирование связей) будет неудачной из-за отсутствия определений функций.
В С++ 1 1 имеется лучший способ достичь по сути того же самого: воспользоваться
конструкцией "= delete': чтобы пометить копирующий конструктор и копирующее при­
сваивание как удаленные функции. Вот та же часть b a s i c ios в С++ 1 1 :
_

template

basic ios ( const bas ic_ios & ) = delete ;
basic ios& operator= ( const basic ios & )

delete ;

};

Отличие удаления этих функций от их объявления как pri vate может показаться больше
вопросом вкуса, чем чем-то иным, но на самом деле в это заложено больше, чем вы думаете.
Удаленные функции не могут использоваться н икоим образом, так что даже код функции­
члена или функций, объявленных как friend, не будет компилироваться, если попытается
копировать объекты b a s i c_ ios. Это существенное улучшение по сравнению с поведением
С++98, где такое некорректное применение функций не диагностируется до компоновки.
По соглашению удаленные функции объявляются как puЫic, а не private. Тому есть
причина. Когда код клиента пытается использовать функцию-член, С++ проверяет до­
ступность до проверки состояния удаленности. Когда клиентский код пытается исполь­
зовать функцию, объявленную как private, некоторые компиляторы жалуются на то,
что это закрытая функция, несмотря на то что доступность функции никак не влияет
на возможность ее использования. Стоит принять это во внимание при пересмотре ста­
рого кода и замене не определенных функций-членов, объявленных как pri vate, удален­
ными, поскольку объявление удаленных функций как puЫ i c в общем случае приводит
к более корректным сообщениям об ошибках.
Важным преимуществом удаленных функций является то, что удаленной может быть
любая функция, в то время как быть pri vate могут только функции-члены. Предпо­
ложим, например, что у нас есть функция, не являющаяся членом, которая принимает
целочисленное значение и сообщает, является ли оно "счастливым числом":
bool isLucky ( int nшnЬer} ;
3.5. Предпочитайте удаленные функции закрытым неоп редел енным

85

То, что С++ является наследником С, означает, что почти любой тип, который можно
рассматривать как отчасти целочисленный, будет неявно преобразовываться в int, но
некоторые компилируемые вызовы могут не иметь смысла:
if ( i sLucky ( ' а ' ) ) ...
if ( isLucky ( t rue ) )
if ( isLucky ( З . 5 ) )

_



11 Является ли ' а ' счастливым числом?
/ / Является ли true счастливым числом?
11 Следует ли вьшолнить усечение до 3
11 перед проверкой на "счастливость " ?

Если счастливые числа действительно должны быть только целыми числами, хотелось
бы предотвратить такие вызовы, как показано выше.
Один из способов достичь этого - создание удаленных перегрузок для типов, кото­
рые мы хотим отфильтровывать:
bool
bool
bool
bool

isLucky ( int nurnЬer ) ;
isLucky ( char )
= dslete;
i sLucky ( bool )
= clelete ;
isLucky (douЬle) = clelete;

11
11
11
11

Исходная функция
Отвергаем символы
Отвергаем булевы значения
Отвергаем douЫe и float

(Комментарий у перегрузки с douЬle, который гласит, что отвергаются как douЬle,
так и f l oat, может вас удивить, но ваше удивление исчезнет, как только вы вспомните,
что при выборе между преобразованием f l oat в i nt и float в douЫe С++ предпочитает
преобразование в douЬle. Вызов i sLucky со значением типа f l oat приведет к вызову
перегрузки с типом douЬle, а не int. Вернее, постарается привести. Тот факт, что данная
перегрузка является удаленной, не позволит скомпилировать такой вызов.)
Хотя удаленные функции использовать нельзя, они являются частью вашей програм­
мы. Как таковые они принимаются во внимание при разрешении перегрузки. Вот почему
при указанных выше объявлениях нежелательные вызовы i sLucky будут отклонены:
if ( i s Lucky ( ' а ' ) ) ... / / Ошибка ! Вызов удаленной функции
if ( isLucky ( true ) ) ... / / Ошибка !
if ( i s1ucky ( 3 . 5 f ) ) ... 11 Ошибка !

Еще один трюк, который могут выполнять удаленные функции (а функции-члены,
объявленные как private - нет), заключается в предотвращении использования ин­
станцирований шаблонов, которые должны быть запрещены. Предположим, например,
что нам нужен шаблон, который работает со встроенными указателями (несмотря на со­
вет из главы 4, "Интеллектуальные указатели", предпочитать интеллектуальные указатели
встроенным):
template
void proces s Pointer (T* ptr ) ;

В мире указателей есть два особых случая. Один из них - указатели void*, поскольку
их нельзя разыменовывать, увеличивать или уменьшать и т.д. Второй - указатели char*,
поскольку они обычно представляют указатели на С-строки, а не на отдельные символы.
Эти особые случаи часто требуют особой обработки; будем считать, что в случае шабло­
на proce s s Pointer эта особая обработка заключается в том, чтобы отвергнуть вызовы
86

Глава 3 . Переход к современному С++

с такими типами (т.е. должно быть невозможно вызвать proce s s Po i nt e r с указателями
типа void* или cha r* ) .
Это легко сделать. Достаточно удалить эти инстанцирования:
template
void processPo inter ( void* )
template
void processPointer ( char* )

delete;

=

delete ;

Теперь, если вызов proce s s Po i n t e r с указателями vo i d* или cha r* является не­
корректным, вероятно, таковым же является и вызов с указателями const void* или
const char*, так что эти инстанцирования обычно также следует удалить:
template
void processPointer ( cons t void* )
template
void processPointer ( const char * )

de lete ;

=

delete;

И если вы действительно хотите быть последовательными, то вы также удалите пе­
регрузки const volat i l e void* и const vola t i l e cha r * , а затем приступите к ра­
боте над перегрузками для указателей на другие стандартные типы символов: wchar_ t,
cha r l б t и cha r32 t .
Интересно, что если у вас есть шаблон функции внутри класса и вы попытаетесь от­
ключить некоторые инстанцирования, объявляя их pri vat e (в духе классического со­
глашения С++98), то у вас ничего не получится, потому что невозможно дать специали­
зации шаблона функции-члена другой уровень доступа, отличный от доступа в главном
шаблоне. Например, если p roce s s Po i n t e r представляет собой шаблон функции-члена
в Widget и вы хотите отключить вызовы для указателей void*, то вот как будет выгля­
деть (не компилируемый) подход С++98:
class Widget
puЫic :
template
void processPointer ( T* ptr )
{ ... }
private :
template
void processPointer ( void* ) ;
};

11 Ошибка !

Проблема заключается в том, что специализации шаблона должны быть написаны
в области видимости пространства имен, а не области видимости класса. Эта пробле­
ма не возникает для удаленных функций, поскольку они не нуждаются в другом уров­
не доступа. Они могут быть удалены за пределами класса (а следовательно, в области
видимости пространства имен):
3.5. Предпочитайте уда л енные функции закрытым неопределенным

87

class Widget
puЫ i c :
template
void proce s s Pointer ( T * pt r )
{ ... )
);

template
1 1 Все еще puЫic, но удаленная
void Widget : : proces s Pointer (void* )
delet e ;
=

Истина заключается в том, что практика С++98 объявления функций private без их
определения в действительности была попыткой добиться того, для чего на самом деле
созданы удаленные функции С++ 1 1 . Будучи всего лишь имитацией, подход С++98 ра­
ботает не так хорошо, как вещь, которую он имитирует. Он не работает вне классов, не
всегда работает внутри классов, а когда работает, то может не работать до компоновки.
Так что лучше придерживаться удаленных функций.
Следует запомн ить


Предпочитайте удаленные функции закрытым функциям без определений.



Удаленной может быть любая функция, включая функции, не являющиеся члена­
ми, и инстанцирования шаблонов.

3 .6. О бъявпяйте перекрывающие функции как override
Мир объектно-ориентированного программирования С++ вращается вокруг классов,
наследования и виртуальных функций. Среди наиболее фундаментальных идей этого
мира - та, что реализации виртуальных функций в производных классах перекрывают
(override) реализации их коллег в базовых классах. Понимание того, насколько легко все
может пойти наперекосяк при перекрытии функций, попросту обескураживает. Полное
ощущение, что эта часть языка создавалась как иллюстрация к законам Мерфи.
Очень часто термин "перекрытие" путают с термином "перегрузка", хотя они совер­
шенно не связаны друг с другом. Поэтому позвольте мне пояснить, что перекрытие вир­
туальной функции - это то, что делает возможным вызов функции производного класса
через интерфейс базового класса:
class Base {
puЫic :
virtual void doWork ( ) ;

11
11

);

class Derived : puЬlic Base {

88

Глава З . Переход к современному С++

Виртуальная функция
базового класса

puЫ i c :
virtual void doWork ( ) ;

11 Перекрывает Base : : doWork
11 Ключевое с.лова virtual

};

/ / здесь не обязательное

std : : unique_pt r upb
11 Указатель на базовый класс
std : :make_unique ( ) ; / / указывает на объект
11 производного класса ;
11 см . std : : make_unique
11 в разделе 4 . 4
=

/ / Вызов doWork через указатель
11 на базовый класс ; вызывается
11 функция производного класса

upb->doWork ( ) ;

Для осуществления перекрытия требуется выполнение нескольких условий.


Функция базового класса должна быть виртуальной.



Имена функций в базовом и производном классах должны быть одинаковыми (за
исключением деструктора).



Типы параметров функций в базовом и производном классах должны быть оди­
наковыми.



Константность функций в базовом и производном классах должна совпадать.



Возвращаемые типы и спецификации исключений функций в базовом и производ­
ном классах должны быть совместимыми.

К этим ограничениям, являющимся частью С++98, С++ 1 1 добавляет еще одно.


Ссьточные квалификаторы функций должны быть идентичными. Ссылочные ква­
лификаторы функции-члена являются одной из менее известных возможностей
С++ 1 1 , так что не удивляйтесь, если вы до сих пор ничего о них не слышали. Они
позволяют ограничить использование функции-члена только объектами lvalue или
только объектами rvalue. Для использования этих квалификаторов функции-чле­
ны не обязаны быть виртуальными.
class Widget
puЫic :
void doWork ( ) & ;

Эта версия
если *this
Эта версия
если * this

doWork применима , только
представляет собой lvalue
doWork применима , только
представляет собой rvalue

};

11
11
11
11

Widget makeWidget ( ) ;

11 Фабричная функция ( возвращает rvalue )

void doWork ( ) &&;

З.б. Объявляйте перекрывающие функции как override

89

Widget w ;

11 Обычный объект ( lvalue )

11
11
11
rnakeWidget ( ) . doWork ( ) ;
11
w . doWork ( ) ;

Вызов
(т.е.
Вызов
(т.е.

Widget :
Widget :
Widget :
Widget :

: doWork
: doWork
: doWork
: doWork

для lvalue
&)
ДЛЯ rvalue
&&)

Позже я расскажу побольше о функциях-членах с о ссылочными квалификатора­
ми, а пока что просто заметим, что если виртуальная функция в базовом клас­
се имеет ссылочный квалификатор, то производный класс, перекрывающий эту
функцию, должен иметь тот же ссылочный квалификатор. Если это не так, объяв­
ленные функции все еще остаются в производном классе, но они ничего не пере­
крывают в базовом классе.
Все эти требования к перекрытию означают, что маленькие ошибки могут привести
к большим последствиям. Код, содержащий ошибки перекрытия, обычно корректен, но
делает совсем те то, что хотел программист. Поэтому в данном случае нельзя полагаться
на уведомления компиляторов о том, что вы что-то делаете неверно. Например, приведен­
ный далее код является абсолютно законным и, на первый взгляд, выглядит разумным,
но в нем нет перекрытия виртуальной функции - нет ни одной функции производного
класса, связанной с функцией базового класса. Сможете ли вы самостоятельно определить,
в чем заключается проблема в каждом конкретном случае (т.е. почему каждая функция
производного класса не переопределяет функцию базового класса с тем же именем)?
class Base {
puЬlic :
virtual void mfl ( ) cons t ;
virtual void mf2 ( int х ) ;
virtual void mf3 ( ) & ;
void mf4 ( ) const ;
};
class Derived : puЫic Base
puЬlic :
virtual void mfl ( ) ;
virtual void mf2 ( unsigned int х ) ;
virtual void mf3 ( ) & & ;
void mf4 ( ) cons t ;
)

;

Нужна помощь?


mfl объявлена как const в классе Base, но в классе Derived этого модификатора нет.



rnf2 получает аргумент типа int в классе Base, но в классе Deri ved она получает

аргумент типа uns igned int.

90

Гn ава 3. Переход к современному Ctt



mfЗ определена с квалификатором lvalue в классе Base и с квалификатором rvalue
в классе De rived.



mf4 не объявлена в классе Base как vi rtua l.

Вы можете подумать "На практике все это вызовет предупреждения компилятора, так
что мне не о чем беспокоиться': Может быть, это и так. А может быть, и не так. В двух из
проверенных мною компиляторах код был принят без единой жалобы - в режиме, когда
все предупреждения были включены. (Другие компиляторы выдавали предупреждения
о некоторых из проблем, но не обо всех одновременно.)
Поскольку очень важно правильно объявить производный класс перекрывающим, но
при этом очень легко ошибиться, C++ l l дает вам возможность явно указать, что функ­
ция производного класса предназначена для того, чтобы перекрывать функцию из базо­
вого класса: ее можно объявить как override. Применяя это ключевое слово к приведен­
ному выше примеру, мы получим следующий производный класс:
class Derived : puЬlic Base (
puЬlic :
virtual void mfl ( ) override;
virtual void mf2 ( unsigned int х ) override;
virtual void mf3 ( ) & & override;
virtual void mf4 ( ) const overricle ;
};

Этот код компилироваться не будет, поскольку теперь компиляторы знают о том, что эти
функции предназначены для перекрытия функций из базового класса, а потому могут
определить наличие описанных нами проблем. Это именно то, что нам надо, и потому вы
должны объявлять все свои перекрывающие функции как override.
Код с использованием ove r r i de, который будет успешно скомпилирован, выглядит
следующим образом (в предположении, что нашей целью является перекрытие функция­
ми в классе Der i ved всех виртуальных функций в классе Base ) :
class Base {
puЬlic :
virtual void
virtual void
virtual void
virtual void
};

mfl ( ) const ;
mf2 ( int х ) ;
mf3 ( ) & ;
mf4 ( ) const ;

class Derived : puЫic Base {
puЫ i c :
vir tual void mf l ( ) const override ;
virtual void mf2 (int х ) override ;
virtual void mf3 ( ) & override ;
void mf4 ( ) const override ; / / Слово virtual не мешает, но
};
/ / и не является обязательным

3.6 . Объяв ляйте перекрывающие функции как override

91

Обратите внимание, что в этом примере часть работы состояла в том, чтобы объявить mf 4
в классе Base как виртуальную функцию. Большинство ошибок, связанных сперекрытием,
совершаются в производном классе, но можно сделать такую ошибку и в базовом классе.
Стратегия использования ключевого слова override во всех перекрытиях производно­
го класса способна на большее, чем просто позволить компиляторам сообщать, когда функ­
ции, которые должны быть перекрытиями, ничего не перекрывают. Они также могут по­
мочь вам оценить последствия предполагаемого изменения сигнатуры виртуальной функ­
ции в базовом классе. Если производные классы везде используют override, вы можете
просто изменить сигнатуру и перекомпилировать систему. Вы увидите, какие повреждения
нанесли своей системе (т.е. сколько классов перестали компилироваться), и после этого
сможете принять решение, стоит ли изменение сигнатуры таких хлопот. Без override вы
должны были бы надеяться на наличие достаточно всеобъемлющих тестов, поскольку, как
мы видели, виртуальные функции, которые предназначены перекрывать функции базового
класса, но не делают этого, не приводят ни к какой диагностике со стороны компилятора.
В С++ всегда имелись ключевые слова, но С++ 1 1 вводит два контекстных ключевых
слова (contextual keywords)
override и f i nal'. Эти ключевые слова являются зарезер­
вированными, но только в некоторых контекстах. В случае override оно имеет зарезерви­
рованное значение только тогда, когда находится в конце объявления функции-члена. Это
значит, что если у вас есть старый код, который уже использовал имя override, его не надо
изменять при переходе к С++ 1 1 :
-

c lass Warning
puЬlic :

11 Потенциально старый класс из С++98

void override ( ) ; / / Корректно как в С++98 , так и
11 ( с тем же смыслом)

в

C+ + l l

};

Это все, что следует сказать об override, но это не все, что следует сказать о ссылоч­
ных квалификаторах функций-членов. Я обещал, что поговорю о них позже, и вот сейчас
как раз и настало это "позже':
Если мы хотим написать функцию, которая принимает только аргументы, являющи­
еся lvalue, мы объявляем параметр, который представляет собой неконстантную \vа\uе­
ссылку:
void doSomething (Widget& w) ; / / Принимает только lvalue Widget

Если же мы хотим написать функцию, которая принимает только аргументы, являющие­
ся rva\ue, мы объявляем параметр, который представляет собой rvalue-ccылкy:
void doSomething (Widget&& w) ; 11 Принимает только rvalue Widget

-' Применение ключевого слова f i n a l к виртуальной функции препятствует перекрытию :ной
функции в производном классе. Ключевое слово final также может быть применено к классу;
в этом случае класс стано11ится неприменимым в качестве базо11ого.
92

Гn ава 3. Переход к современному С++

Ссылочные квалификаторы функции-члена позволяют проводить такое же различие
для объектов, функции-члены кторых вызываются, т.е. * t h i s . Это точный аналог мо­
дификатора const в конце объявления функции-члена, который указывает, что объект,
для которого вызывается данная функция-член (т.е. *thi s), является const.
Необходимость в функциях-членах со ссылочными квалификаторами нужна не так
уж часто, но может и возникнуть. Предположим, например, что наш класс Widget име­
ет член-данные std : : vector, и мы предлагаем функцию доступа, которая обеспечивает
клиентам к нему непосредственный доступ:
class Widget {
puЫi c :
using DataType

std : : vector; / / См . информацию о
11 using в разделе 3 . 3

DataType& data ( ) { return value s ; }
private :
DataType values ;
};

Вряд ли это наиболее инкапсулированный дизайн, который видел свет, но оставим
этот вопрос в стороне и рассмотрим, что происходит в следующем клиентском коде:
Widget w;
auto valsl

=

w . data ( ) ;

/ / Копирует w . values в valsl

Возвращаемый тип W i dget : : data представляет собой \vа\uе-ссылку (чтобы быть точ­
ным - std: : vector&), а поскольку \vа\uе-ссылки представляют собой lva\ue, мы
инициализируем v a l s l из \vа\uе. Таким образом, val s l создается копирующим конструк­
тором из w . va lues, как и утверждает комментарий.
Теперь предположим, что у нас имеется фабричная функция, которая создает Widget:
Widget makeWidget ( ) ;

и мы хотим инициализировать переменную с помощью s t d : : vector в W i dget, возвра­
щенном из makeWidget:
auto vals2

=

makeWidget ( ) . data ( } ; // Копирование значений в
// Widget в vals2

И вновь Widget s : : data возвращает \vа\uе-ссылку, и вновь \vа\uе-ссылка представляет
собой lvalue, так что наш новый объект (va l s 2 ) опять является копией, построенной из
va lues в объекте Widget. Однако в этот раз Widget представляет собой временный объект,
возвращенный из ma keWidget (т.е. представляет собой rvalue), так что копирование в него
std : : vector представляет собой напрасную трату времени. Предпочтительнее выполнить
перемещение, но, поскольку data возвращается как \vа\uе-ссылка, правила С++ требуют,
чтобы компиляторы генерировали код для копирования. (Имеется некоторый маневр

3.6. Объяв л яйте перекрывающие функции как override

93

для оптимизации на основе правила "как если бы"4, но было бы глупо полагаться та то, что
ваш компилятор найдет способ им воспользоваться.)
Нам необходим способ указать, что, когда data вызывается для Widget, являющего­
ся rvalue, результат также будет представлять собой rvalue. Использование ссылочных
квалификаторов для перегрузки data для Widget, являющихся lvalue и rvalue, делает это
возможным:
class Widget
puЫic :
using DataType

std : : vector;

DataType& data ( ) &
{ return values ; }
DataType&& data ( ) &&
{ return std: : move (values ) ;

/ / Для lvalue
11 возвращает
11 Для rvalue
11 возвращает

Widge t ,
lvalue
Widge t ,
rvalue

private :
DataType value s ;
};

Обратите внимание на разные возвращаемые типы перегрузок data. Перегрузка
для lvаluе-ссылки возвращает lvalue-ccылкy (т.е. lvalue), а перегрузка для rvаluе-ссылки
возвращает rvalue-ccылкy (которая, как возвращаемый тип функции, является rvalue).
Это означает, что клиентский код ведет теперь себя так, как мы и хотели:
auto val s l

auto val s 2

=

/ / Вызывает lvаluе - перегрузку
11 Widget : : data, va lsl
11 создается копированием
makeWidget ( ) . data ( ) ; / / Вызывает rvаluе - перегрузку
11 Widget : : data, val s 2
/ / создается перемещением
w . data ( ) ;

Это, конечно, хорошо, но не позвольте теплому сиянию этого хэппи-энда отвлечь вас
от истинной цели этого раздела. Эта цель в том, чтобы убедить вас, что всякий раз, когда
вы объявляете в производном классе функцию, предназначенную для перекрытия вирту­
альной функции базового класса, вы не забывали делать это с использованием ключевого
слова overr i de.
Кстати, если функция-член использует ссылочный квалификатор, все перегрузки этой
функции также должны использовать его. Это связано с тем, что перегрузки без этих
квалификаторов могут вызываться как для объектов lvalue, так и для объектов rvalue.
Такие перегрузки будут конкурировать с перегрузками, имеющими ссылочные квалифи­
каторы, так что все вызовы функции будут неоднозначными.

if rule"
правило, согласно которому разрешены любые преобразования кода, не изменяю­
щие наблюдаемое 11оведение программы. Примеч. ред.

4 "As

-

-

94

Глава 3. Переход к современному С++

Сл едует зап омнить


Объявляйте перекрывающие функции как over ride.



Ссылочные квалификаторы функции-члена позволяют по-разному рассматри­
вать lvalue- и rvаluе-объекты ( * th i s ) .

3 .7. Предпочитайте итераторы const_iterator
итераторам i tera tor
Итераторы const _ i t e ra t o r представляют собой SТL-эквивалент указателя на con s t .
Они указывают н а значения, которые н е могут быть изменены. Стандартная практика
применения const там, где это только возможно, требует применения c o n s t_ i t e ra t o r
везде, где нужен итератор, н о не требуется изменять то, н а что этот итератор указывает.
Это верно как для С++98, так и для C++ l l , но в С++98 поддержка const_i t e ra t o r
носит половинчатый характер. Такие итераторы н е так легко создавать, а если у вас уже
имеется такой итератор, его использование весьма ограничено. Предположим, например,
что вы хотите выполнить в s t d : : vect o r < i n t > поиск первого встречающегося значения
1983 (год, когда название языка программирования "С с классами" сменилось на С++),
а затем вставить в это место значение 1 998 (год принятия первого ISО-стандарта С++).
Если в векторе нет значения 1 983, вставка выполняется в конец вектора. При использо­
вании итераторов i t e ra t o r в С++98 сделать описанное просто:
std: : vector value s ;

s t d : : vector : : iterator i t
std : : find ( values . begin ( ) , value s . end ( ) , 1 9 8 3 ) ;
values . insert ( it , 1 998 ) ;
=

Но i t e ra t o r - в данном слу 1 1 размер указателя на функцию !
makelnvestment (Ts&& . . . params ) ;
template

std: : Шlique__p tr }, а другую - для массивов
(std : : unique _pt r). В результате никогда не возникает неясность в том, на ка­
кую сущность указывает s t d : : un i que_p t r. API s t d : : un i que_p t r разработан так,
чтобы соответствовать используемой разновидности. Например, в случае указателя
для одного объекта отсутствует оператор индексирования (operator [ ] ), в то время
как в случае указателя для массива отсутствуют операторы разыменования (ope rator*
и operator- >).
Существование std : : unique_pt r для массивов должно представлять только интел­
лектуальный интерес, поскольку std : : array, std : : vector и std : : s tring почти всегда
оказываются лучшим выбором, чем встроенные массивы. Я могу привести только одну
ситуацию, когда s t d : : unique_pt r имеет смысл - при использовании С-образного
API, который возвращает встроенный указатель на массив в динамической памяти, ко­
торым вы будете владеть.
Интеллектуальный указатель std : : unique _ptr представляет собой способ выражения
исключительного владения на С++ 1 1 , но одной из наиболее привлекательных возможно­
стей является та, что его можно легко и эффективно преобразовать в std : : shared_ptr:
11 Конвертация std : : unique_pt r
makeinvestment ( arguments ) ; 11 в std : : sha red_pt r

std: : shared_ytr sp

=

Это ключевой момент, благодаря которому std : : unique_pt r настолько хорошо под­
ходит для возвращаемого типа фабричных функций. Фабричные функции не могут знать,
будет ли вызывающая функция использовать семантику исключительного владения воз­
вращенным объектом или он будет использоваться совместно (т.е. std : : shared_pt r).
Возвращая s t d: : un i que_pt r, фабрики предоставляют вызывающим функциям наиболее
эффективный интеллектуальный указатель, но не мешают им заменить этот указатель
более гибким. (Информация об интеллектуальном указателе s t d : : shared_ptr приведена
в разделе 4.2.)
Сл едует запомнить


s t d : : u n i que_pt r представляет собой маленький, быстрый, предназначенный
только для перемещения интеллектуальный указатель для управления ресурсами
с семантикой исключительного владения.



По умолчанию освобождение ресурсов выполняется с помощью оператора delete,
но могут применяться и пользовательские удалители. Удалители без состояний
и указатели на функции в качестве удалителей увеличивают размеры объектов
std : : unique_ptr.



Интеллектуальные указатели std : : unique_pt r легко преобразуются в интеллекту­
альные указатели std : : shared_pt r.

1 32

Глава 4. И нтеллектуальные указатели

4.2 . Испопьзуйте s td : : sharedytr дпя управпения
ресурсами путем совместноrо впадения
Программисты на языках программирования со сборкой мусора показывают паль­
цами на программистов на С++ и смеются над ними, потому что те озабочены предот­
вращением утечек ресурсов. "Какой примитив! - издеваются они. - Вы что, застря­
ли в 1 960-х годах и в Lisp? Управлять временем жизни ресурсов должны машины, а не
люди': Разработчики на С++ не остаются в долгу: "Вы имеете в виду, что единственным
ресурсом является память, а время освобождения ресурса должно быть принципиально
неопределимо? Спасибо, мы предпочитаем обобщенность и предсказуемость деструкто­
ров!" Впрочем, в нашей браваде есть немного бахвальства. Сборка мусора в действитель­
ности достаточно удобна, а управление временем жизни вручную действительно может
показаться похожим на создание микросхем памяти из шкуры медведя с помощью ка­
менных ножей. Почему же не получается взять лучшее из двух миров - систему, которая
работает автоматически (как сборка мусора) и к тому же применима ко всем ресурсам
и имеет предсказуемое время выполнения (подобно деструкторам)?
Интеллектуальный указатель s t d : : shared_pt r представляет собой способ, кото­
рым С++ 1 1 объединяет эти два мира. Объект, доступ к которому осуществляется че­
рез указатели std : : sha red_ptr, имеет время жизни, управление которым осущест­
вляется этими указателями посредством совместного вла дения. Никакой конкретный
указатель std : : shared_pt r не владеет данным объектом. Вместо этого все указатели
std : : shared_pt r, указывающие на него, сотрудничают для обеспечения гарантии, что
его уничтожение произойдет в точке, где он станет более ненужным. Когда последний
указатель s t d : : shared_pt r, указывающий на объект, прекратит на него указывать (на­
пример, из-за того, что этот st d : : shared_pt r будет уничтожен или перенаправлен
на другой объект), этот std : : shared_pt r уничтожит объект, на который он указывал.
Как и в случае сборки мусора, клиентам не надо самим беспокоиться об управлении вре­
менем жизни объектов, на которые они указывают, но, как и при работе с деструктора­
ми, время уничтожения объекта оказывается строго определенным.
Указатель s t d : : shared_ptr может сообщить, является ли он последним указателем,
указывающим на ресурс, с помощью счетчика ссылок, значения, связанного с ресурсом
и отслеживающего, какое количество указателей std : : shared_pt r указывает на него.
Конструкторы std : : shared_pt r увеличивают этот счетчик (обычно увеличивают - см.
ниже), деструкторы std : : shared_pt r уменьшают его, а операторы копирующего присва­
ивания делают и то, и другое. (Если spl и sp2 являются указателями std : : shared_ptr,
указывающими на разные объекты, присваивание "spl
sp2 ; " модифицирует spl так,
что он указывает на объект, на который указывает sp2. Конечным результатом присва­
ивания является то, что счетчик ссылок для объекта, на который изначально указывал
spl, уменьшается, а значение счетчика для объекта, на который указывает sp2, увели­
чивается.) Если std : : shared_pt r после выполнения декремента видит нулевой счетчик
ссылок, это означает, что на ресурс не указывает больше ни один std : : shared_pt r, так
что наш интеллектуальный указатель освобождает этот ресурс.
=

4.2. Используйте std::sha red_ptr для управления ресурсами путем совместноrо владения

1 33

Наличие счетчиков ссылок влияет на производительность.


Размер std : : shared_y tr в два раза больше размера обычноrо указателя, по­
скольку данный интеллектуальный указатель содержит обычный указатель на ре­
сурс и другой обычный указатель на счетчик ссылок2•



Память для счетчика ссылок должна выделяться динамически. Концепту­
ально счетчик ссылок связан с объектом, на который он указывает, однако сам
указываемый объект об этом счетчике ничего не знает. В нем нет места для хра­
нения счетчика ссылок. (Приятным следствием этого является то, что интеллек­
туальный указатель s t d : : shared_pt r может работать с объектами любого типа
(в том числе встроенных типов).) В разделе 4.4 поясняется, что можно избежать
стоимости динами•1еского выделения при создании указателя std : : shared_pt r
с помощью вызова std : : make_shared, однако имеются ситуации, когда функция
s t d : : make _ shared неприменима. В любом случае счетчик ссылок хранится в ди­
намически выделенной памяти.



поскольку
могут присутствовать одновременное чтение и запись в разных потоках. Напри­
мер, s t d : : shared_ptr, указывающий на ресурс в одном потоке, может выполнять
свой деструктор (тем самым уменьшая количество ссылок на указываемый им ре­
сурс), в то время как в другом потоке указатель std : : shared_ptr на тот же объ­
ект может быть скопирован (а следовательно, увеличивает тот же счетчик ссылок).
Атомарные операции обычно медленнее неатомарных, так что несмотря на то, что
обычно счетчики ссылок имеют размер в одно слово, следует рассматривать их
чтение и запись как относительно дорогостоящие операции.
Инкремент и декремент счетчика ссылок должны быть атомарными,

Возбудил ли я ваше любопытство, когда написал, что конструкторы std : : shared_
p t r "обычно" увеличивают счетчик ссылок для указываемого объекта? При
созданииstd : : shared_pt r, указывающего на объект, всегда добавляется еще один ин­
теллектуальный указатель std : : sha red_ptr, так почему же счетчик ссылок может уве­
личиваться не всегда?
Из-за перемещающего конструирования - вот почему. Перемещающее конструиро­
вание указателя s t d : : shared_pt r из другого s t d : : shared_pt r делает исходный указа­
тель нулевым, а это означает, что старый s t d : : shared_ptr перестает указывать на ре­
сурс в тот же момент, в который новый std : : shared_ptr начинает это делать. В резуль­
тате изменение значения счетчика ссылок не требуется. Таким образом, перемещение
std : : sha red_pt r оказывается быстрее копирования: копирование требует увеличения
счетчика ссылок, а перемещение - нет. Это справедливо как для присваивания, так
и для конструирования, так что перемещающее конструирование быстрее копирующего
конструирования, а перемещающее присваивание быстрее копирующего присваивания.

2 Стандарт не требует испопьзования именно такой реапизации, но все известные мне реапизации
стандартной бибпиотеки ноступают именно так.
1 34

Глава 4. Интеллектуальные указатели

Подобно std : : unique_pt r (см. раздел 4. l ), std : : shared_pt r в качестве механизма
удаления ресурса по умолчанию использует delete, но поддерживает и пользовательские
удалители. Однако дизайн этой поддержки отличается от дизайна для std : : un i que_ptr.
Для s t d : : unique_pt r тип удалителя является частью типа интеллектуального указателя.
Для std : : shared_ptr это не так:
auto loggingDel

=

[ ] ( Widget *pw ) / / Пользовательский удалитель

/ / ( как в разделе 4 . 1 )

{

makeLogEntry ( pw ) ;
delete pw;
1;

std: : unique_ytr<
/ / Тип удалителя является
Widget, decltype (logqinqDel) // частью типа указателя
> upw ( new Widget, loggingDel ) ;
/ / Тип удалителя не является
std: : shared_ytr
spw ( new Widget , loggingDe l ) ; / / частью типа указателя

Дизайн std : : shared_pt r более гибок. Рассмотрим два указателя std : : shared_pt r
, каждый со своим пользовательским удалителем разных типов (например, из-за
того, что пользовательские удалители определены с помощью лямбда-выражений):
auto customDeleterl
auto customDeleter2

[]
[]

(Widget *pw)
( Widget *pw)

f ; 1 1 Пользовательские
f ; 1 1 удалители
1 1 разных типов

std : : shared_ptr pwl ( new Widget , customDeleterl ) ;
std : : shared_ptr pw2 ( new Widget , customDeleter2 ) ;

Поскольку pwl и pw2 имеют один и тот же тип, они могут быть помещены в контейнер
объектов этого типа:
std : : vector vpw { pwl , pw2 f ;

Они также могут быть присвоены один другому и переданы функции, принимающей
параметр типа s t d : : shared_ptr. Ни одно из этих действий не может быть вы­
полнено с указателями std : : uni que_ptr, которые отличаются типами пользовательских
удалителей, так как эти типы влияют на тип самого std : : uni que_ptr.
Друтим отличием от s t d : : uni que_pt r является то, что указание пользовательского
удалителя не влияет на размер объекта s t d : : shared_pt r. Независимо от удалителя объект
std : : shared_pt r имеет размер, равный размеру двух указателей. Это хорошая новость, но
она должна привести вас в замешательство. Пользовательские удалители могут быть функ­
циональными объектами, а функциональные объекты могут содержать любое количество
данных, а значит, быть любого размера. Как же std : : shared_pt r может обращаться к уда­
лителю произвольного размера, не используя при этом дополнительной памяти?

4.2.

Используйте std::shared_ptr дnя управления ресурсами путем совместного впадения

1 35

А он и не может. Ему приходится использовать дополнительную память, но эта па­
мять не является частью объекта s t d : : sha red_pt r. Она располагается в динамической
памяти или, если создатель s t d : : shared_pt r воспользуется поддержкой со стороны
s t d : : shared_p t r пользовательских распределителей памяти, там, где выделит память
такой распределитель. Ранее я отмечал, что объект std : : shared_ptr содержит указатель
на счетчик ссылок для объекта, на который он указывает. Это так, но это немного не
так, поскольку счетчик ссылок является частью большей структуры данных, известной
под названием управляющий блок (control Ыосk). Управляющий блок имеется для каждо­
го объекта, управляемого указателями s t d : : shared_ptr. Управляющий блок в дополне­
ние к счетчику ссылок содержит копию пользовательского удалителя, если таковой был
указан. Если указан пользовательский распределитель памяти, управляющий блок содер­
жит и его копию. Управляющий блок может также содержать дополнительные данные,
включающие, как поясняется в разделе 4.4, вторичный счетчик ссылок, известный как
слабый счетчик, но в данном разделе мы его игнорируем. Мы можем представить память,
связанную с объектом s t d : : shared_ptr, как имеющую следующий вид:
std : : shared ptr
Указатеnь на т

Управляющий блок объекта настраивается функцией, создающей первый указатель
std : : shared_ptr на объект. Как минимум это то, что должно быть сделано. В общем
случае функция, создающая указатель std : : shared ptr на некоторый объект, не может
знать, не указывает ли на этот объект некоторый другой указатель s t d : : shared_pt r, так
что при создании управляющего блока должны использоваться следующие правила.


Функция std : : make_shared (см. раздел 4.4) всегда создает управляющий блок.

Она производит новый объект, на который будет указывать интеллектуальный
указатель, так что в момент вызова std : : make_shared управляющий блок для это­
го объекта, определенно, не существует.


Управляющий блок создается тогда, когда указатель std : : shared_ptr соз­
дается из указателя с исключительным владением (т.е. std : : unique_ptr или
std : : auto_ptr). Указатели с исключительным владением не используют управля­
ющие блоки, так что н икакого управляющего блока для указываемого объекта не
существует. (Как часть своего построения std : : sha red_pt r осуществляет владе­
ние указываемым объектом, так что указатель с исключительным владением ста­
новится нулевым.)

1 36

Гnава 4. Интеnnектуаnьные указатеnи



Когда конструктор std : : sharedytr вызывается с обычным указателем, он

Если вы хотите создать std : : shared_ptr из объек­
та, у которого уже имеется управляющий блок, вы предположительно передаете
в качестве аргумента конструктора std : : shared_pt r или std : : weak_ptr (см. раз­
дел 4.3), а не обычный указатель. Конструкторы std : : shared_pt r, принимающие
в качестве аргументов указатели std : : shared_ptr или std : : weak_pt r, не создают
новые управляющие блоки, поскольку могут воспользоваться управляющими бло­
ками, на которые указывают переданные им и нтеллектуальные указатели.
создает управляющий блок.

Следствием из этих правил является то, что создание более одного std : : shared_ptr
из единственного обычного указателя дает вам бесплатный билет для путешествия в не­
определенное поведение, поскольку в результате указываемый объект будет иметь не­
сколько управляющих блоков. Несколько управляющих блоков означают несколько счет­
чиков ссылок, а несколько счетчиков ссылок означают, что объект будет удален несколь­
ко раз (по одному для каждого счетчика ссылок). И все это значит, что приведенный
ниже код плох, ужасен, кошмарен:
auto pw

=

new Widget ;

11 pw

-

обычный указатель

std : : shared_ptr
spwl (pw loggingDe l ) ; / / Создание управляющего блока для *pw
,

std: : shared_ptr
/ / Создание второго
spw2 (pw, loggingDe l ) ; / / управляющего блока для *pw !

Создание обычного указателя pw, указывающего н а динамически выделенный объ­
ект, - плохое решение, поскольку оно противоречит главному совету всей главы: пред­
почитайте интеллектуальные указатели обычным указателям. (Если вы забыли, откуда
взялся этот совет, заново прочтите первую страницу данной главы.) Но пока что забудем
об этом. Строка, в которой создается pw, представляет собой стилистическую мерзость,
но она по крайней мере не приводит к неопределенному поведению.
Далее вызывается конструктор для spwl, которому передается обычный указатель, так
что этот конструктор создает управляющий блок (и тем самым счетчик ссылок) для того,
на что он указывает. В данном случае это *pw (т.е. объект, на который указывает pw).
Само по себе это не является чем-то страшным, но далее с тем же обычным указателем
вызывается конструктор для spw2, и он также создает управляющий блок (а следователь­
но, и счетчик ссылок) для *pw. Объект *pw, таким образом, имеет два счетчика ссылок,
каждый из которых в конечном итоге примет нулевое значение, и это обязательно приве­
дет к попытке уничтожить объект *pw дважды. Второе уничтожение и будет ответствен­
но за неопределенное поведение.
Из этого можно вынести как минимум два урока, касающиеся применения
std : : shared_ptr. Во-первых, пытайтесь избегать передачи обычных указателей конструк­
тору std : : shared ptr. Обычной альтернативой этому является применение функции
std : : make_shared (см. раздел 4.4), но в примере выше мы использовали пользовательские

4.2.

Используйте std::shared_ptr дл я управления ресурсами путем совместноrо владения

1 37

удалители, а это невозможно при использовании std : : make_shared. Во-вторых, если вы
вынуждены передавать обычный указатель конструктору std : : shared_ptr, передавайте
непосредственно результат оператора new, а не используйте переменную в качестве по­
средника. Если первую часть кода переписать следующим образом,
s td : : shared_ptr
spwl (new Widget, / / Непосредственное использование new
loggingDel) ;

то окажется гораздо труднее создать второй std : : shared_ptr из того же обычного ука­
зателя. Вместо этого автор кода, создающего spw2, естественно, будет использовать в ка­
честве аргумента инициализации spwl (т.е. будет вызывать копирующий конструктор
s t d : : shared_pt r), и это не будет вести ни к каким проблемам:
std : : shared_ptr
spw2 ( spwl ) ;

11 spw2 использует тот же
/ / управляющий блок, что и spwl

Особенно удивительный способ, которым применение переменных с обычными ука­
зателями в качестве аргументов конструкторов std : : shared_pt r может привести к мно­
жественным управляющим блокам, включает указатель t h i s . Предположим, что наша
программа использует указатели std : : shared_pt r для управления объектами Widget и у
нас есть структура данных, которая отслеживает объекты Widget, которые были обрабо­
таны:
std : : vector p roce ss edWidget s ;

Далее, предположим, что Widget имеет функцию-член, выполняющую эту обработку:
class Widget
puЫic :
void process ( ) ;
};

Вот вполне разумно выглядящий подход к написанию Widget : : process:
void Widget : : process ( )
{
1 1 Обработка Widget
processedWidgets . emplace_back (this) ; / / Добавление в список
/ / обработанных Widge t ;
1 1 это неправильно !

Комментарий о неправильности кода относится не к использованию empl ace_back,
а к передаче t h i s в качестве аргумента. (Если вы не знакомы с emp l ace_back, обрати­
тесь к разделу 8.2.) Этот код будет компилироваться, но он передает обычный указатель
(this) контейнеру указателей std : : shared_ptr. Конструируемый таким образом ука­
затель std : : shared_pt r будет создавать новый управляющий блок для указываемого

1 38

Гn ава 4. Интеnnектуаnьные указатеnи

Widget (т.е. для объекта * t h i s). Это не выглядит ужасным до тех пор, пока вы не по­
нимаете, что, если имеются указатели std : : shared_ptr вне функции-члена, которые уже
указывают на этот объект Widget, вам не избежать неопределенного поведения.
API s t d : : s ha r e d _pt r включает средство для ситуаций такого рода. Веро­
ятно, его имя - наиболее странное среди всех имен стандартной библиотеки:
std : : еnаЫе shared_from_ t h i s. Это шаблон базового класса, который вы наследуете,
если хотите, чтобы класс, управляемый указателями s t d : : shared_pt r, был способен
безопасно создавать std : : shared_ptr из указателя this. В нашем примере Widget будет
унаследован от std : : еnаЫе_shared_ from_this следующим образом:
_

class Widget : puЬlic std: : enaЫe_shared_from_this
puЫic :
void process ( ) ;
};

Как я уже говорил, s t d : : еnаЫе shared_ from_t h i s является шаблоном базового
класса. Его параметром типа всегда является имя производного класса, так что класс
Widget порождается от std : : enaЬle shared_ from_ this. Если идея произво­
дного класса, порожденного от базового класса, шаблонизированного производным, вы­
зывает у вас головную боль, попытайтесь об этом не думать. Этот код совершенно закон­
ный, а соответствующий ш аблон проектирования хорошо известен, имеет стандартное
имя, хотя и почти такое же странное, как std : : еnаЫе_shared_ from this. Это имя Странно повторяющийся шаблон ( The Curiously Recurring Teтplate Pattern - CRTP). Если
вы хотите узнать о нем побольше, расчехлите свой поисковик, поскольку сейчас мы воз­
вращаемся к нашему барану по имени std : : еnаЫе_shared from t h i s.
Шаблон s t d : : enaЬ l e_ shared_ from_ this определяет функцию-член, которая создает
std : : shared_pt r для текущего объекта, но делает это, не дублируя управляющие блоки.
Это функция-член shared_from this, и вы должны использовать ее в функциях-членах
тогда, когда вам нужен s t d : : shared_ptr, который указывает на тот же объект, что и ука­
затель this. Вот как выглядит безопасная реализация W idget : : p rocess:
_

_

_

_

_

void Widget : : process ( )
{
/ / Как и ранее , обработка Widget
/ / Добавляем std : : shared_ptr, указывающий на
/ / текущий объект, в processedWidgets
processedWidgets . emplace_back ( shared_from_this ( ) ) ;

Внутри себя shared_ from_this ищет управляющий блок текущего объекта и создает
новый std: : shared ptr, который использует этот управляющий блок. Дизайн функции
полагается на тот факт, что текущий объект имеет связанный с ним управляющий блок.
Чтобы это было так, должен иметься уже существующий указатель std : : sha red_p t r
4 .2. Испоnьзуйте std::shared_ptr дnя управnения ресурсами путем совместного впадения

1 39

(например, за пределами функции-члена, вызывающей shared from this ) , который ука­
зывает на текущий объект. Если такого s t d : : shared_ptr нет (т.е. если текущий объект
не имеет связанного с н им управляющего блока), результатом будет неопределенное по­
ведение, хотя обычно shared_ from_this генерирует исключение.
Чтобы препятствовать клиентам вызывать функции-члены, в которых используется
sha red_from_this, до того как на объект будет указывать указатель s t d : : shared_ptr,
классы, наследуемые от std : : enaЫe_ shared_ from_t h i s , часто объявляют свои кон­
структоры как pri vate и заставляют клиентов создавать объекты путем вызова фабрич­
ных функций, которые возвращают указатели std : : shared_ptr. Например, класс Widget
может выглядеть следующим образом:
class Widge t : puЬ l i c std: : enaЬle_shared_ from_this
puЬlic :
11 Фабричная функция, пересылающая
11 аргументы закрытому конструктору :
teшplate
static std: : shared_ytr create (Ts&& . . . params) ;

void process ( ) ;

11

Как и ранее

11

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

private :
1;

В настоящее время вы можете только смутно припоминать, что наше обсуждение
управляющих блоков было мотивировано желанием понять, с какими затратами связано
применение s t d : : shared_ptr. Теперь, когда мы понимаем, как избегать создания слиш­
ком большого количества управляющих блоков, вернемся к нашей первоначальной теме.
Управляющий блок обычно имеет размер в несколько слов, хотя пользовательские
удалители и распределители памяти могут его увеличить. Обычная реализация управ­
ляющего блока оказывается более интеллектуальной, чем можно было бы ожидать. Она
применяет наследование, и при этом даже имеются виртуальные функции. (Все это тре­
буется для того, чтобы обеспечить корректное уничтожение указываемого объекта.) Это
означает, что применение указателей std : : shared_pt r берет на себя также стоимость
механизма виртуальной функции, используемой управляющим блоком.
Возможно, после того как вы прочли о динамически выделяемых управляющих бло­
ках, удалителях и распределителях неограниченного размера, механизме виртуальных
функций и атомарности работы со счетчиками ссылок, ваш энтузиазм относительно
s t d : : shared_ptr несколько угас. Это нормально.
Они не являются наилучшим решением для любоf! задачи управления ресурсами. Но
для предоставляемой ими функциональности цена std : : shared_pt r весьма разумна.
В типичных условиях, когда использованы удалитель и распределитель памяти по умол­
чанию, а s t d : : shared_pt r создается с помощью s t d : : ma ke_shared, размер управляю­
щего блока составляет около трех слов, и его выделение, по сути, ничего не стоит. (Оно

1 40

Глава 4. Интеллектуальные укаэатели

встроено в выделение памяти для указываемого им объекта. Дополнительная информа­
ция об этом приведена в разделе 4.4.) Разыменование std : : shared_ptr не более дорого­
стояще, чем разыменование обычного указателя. Выполнение операций, требующих ра­
боты со счетчиком ссылок (например, копирующий конструктор или копирующее при­
сваивание, удаление) влечет за собой одну или две атомарные операции, но эти операции
обычно отображаются на отдельные машинные команды, так что, хотя они могут быть
дороже неатомарных команд, они все равно остаются отдельными машинными команда­
ми. Механизм виртуальных функций в управляющем блоке обычно используется только
однажды для каждого объекта, управляемого указателями s t d : : s hared_ptr: когда про­
исходит уничтожение объекта.
В обмен на эти весьма скромные расходы вы получаете автоматическое управление
временем жизни динамически выделяемых ресурсов. В большинстве случаев применение
std : : shared_ptr значительно предпочтительнее, чем ручное управление временем жиз­
ни объекта с совместным владением. Если вы сомневаетесь, можете ли вы позволить себе
использовать s t d : : shared_ptr, подумайте, точно ли вам нужно обеспечить совместное
владение. Если вам достаточно или даже может бь1ть достаточно исключительного
владения, лучшим выбором является std : : unique ptr. Его профиль производитель­
ности близок к таковому для обычных указателей, а "обновление" std : : un i que_pt r
до std : : shared_ptr выполняется очень легко, так как указатель std : : shared_ptr может
быть создан из указателя std : : unique_ptr.
Обратное неверно. После того как вы включили управление временем жизни ресур­
са с помощью std : : shared_pt r, обратной дороги нет. Даже если счетчик ссылок ра­
вен единице, нельзя вернуть владение ресурсом для того, чтобы, скажем, им управлял
std : : unique_ptr. Контракт владения между ресурсом и указателями s t d : : sha red_ptr,
которые указывают на ресурс, написан однозначно - "пока смерть не разлучит нас': Ни­
каких разводов и раздела имущества не предусмотрено.
Есть еще кое-что, с чем не могут справиться указатели std : : shared_pt r, - массивы.
Это еще одно их отличие от указателей std : : unique_ptr. Класс s t d : : shared_ptr имеет
API, предназначенное только для работы с указателями на единственные объекты. Не су­
ществует s t d : : shared_ptr. Время от времени "крутые" программисты натыкаются
на мысль использовать std : : shared_ptr для указания на массив, определяя пользо­
вательский удалитель для выполнения освобождения массива (т.е. delete [ ] ). Это можно
сделать и скомпилировать, но это ужасная идея. С одной стороны, класс std: : shared_pt r
не предоставляет оператор operator [ ] , так что индексирование указываемого массива
требует неудобных выражений с применением арифметики указателей. С другой сторо­
ны, std : : shared_ptr поддерживает преобразование указателей на производные классы
в указатели на базовые классы, которое имеет смысл только для одиночных объектов,
но при применении к массивам оказывается открытой дырой в системе типов. (По этой
причине API std : : unique_ptr запрещает такие преобразования.) Что еще более
важно, учитывая разнообразие альтернатив встроенным массивам в С++ 1 1 (например,
std : : array, std : : vector, std : : string), объявление интеллектуального указателя на ту­
пой массив всегда является признаком плохого проектирования.

4.2.

Используйте std::shared_ptr для управления ресурсами путем совместноrо владения

1 41

Следует запомн ить


s t d : : sha red_p t r предоставляет удобный подход к управлению временем жизни
произвольных ресурсов, аналогичный сборке мусора.



По сравнению с s t d : : un i que_p t r объекты st d : : shared_p t r обычно в два раза
больше, привносят накладные расходы на работу с управляющими блоками и тре­
буют атомарной работы со счетчиками ссылок.



Освобождение ресурсов по умолчанию выполняется с помощью оператора delete,
однако поддерживаются и пользовательские удалители. Тип удалителя не влияет
на тип указателя s t d : : shared_pt r.



Избегайте создания указателей s t d : : sha red_p t r из переменных, тип которых обычный встроенный указатель.

4.3 . Испопьзуйте s td : : weak_ptr
дпя s td : : shared_рtr- подобных указа тепей,
которые моrут бы ть висячими
Парадоксально, но может быть удобно иметь интеллектуальный указатель, работающий
как s t d : : sha red_pt r (см. раздел 4.2), но который не участвует в совместном владении ре­
сурсом, на который указывает (друтими словами, указатель наподобие s t d : : sha red_pt r,
который не влияет на счетчик ссылок объекта). Эта разновидность интеллектуального ука­
зателя должна бороться с проблемой, неизвестной указателям s td : : sha red _pt r: возмож­
ностью того, что объект, на который он указывает, был уничтожен. Истинный интеллекту­
альный указатель в состоянии справиться с этой проблемой, отслеживая, когда он стано­
вится висячим, т.е. когда объект, на который он должен указывать, больше не существует.
Именно таковым и является интеллектуальный указатель s t d : : weak_pt r.
Вы можете удивиться, зачем может быть нужен указатель std : : weak_pt r. Вы, вероятно,
удивитесь еще больше, когда познакомитесь с его API. Указатель std: : weak ptr не может
быть ни разыменован, ни проверен на "нулевость': Дело в том, что std: : wea k_pt r не явля­
ется автономным интеллектуальным указателем. Это - дополнение к std : : sha red_ptr.
Их взаимосвязь начинается с самого рождения: указатели std: : weak_pt r обычно соз­
даются из указателей s td : : shared_pt r. Они указывают на то же место, что и инициа­
лизирующие их указатели std : : shared_pt r, но не влияют на счетчики ссылок объекта,
на который указывают:
//
auto spw =
std : : make_shared ( ) ; //
11
//

После создания spw счетчик
ссылок указываемого Widget
равен 1 . ( 0 std: :make_shared
см . раздел 4 . 4 . )

std: : weak_ytr wpw ( spw) ; / / wpw указывает на тот же
/ / Widge t , что и spw . Счетчик

1 42

Гла ва 4. Интеллектуальные указатели

/ / ссЫJ1ок остается равным 1
spw

11 Счетчик ссЫJ1ок равен О , и
1 1 Widget уничтожается .
11 wpw становится висячим

nullpt r ;

О висячем s t d : : wea k_pt r говорят, что он
это непосредственно:
if (wpw . expired ( ) )

"

просрочен

(expired). Вы можете проверить

. // Если wpw не указывает на объект".

Но чаше всего вам надо не просто проверить, не просрочен ли указатель s t d : : weak_pt r,
но и, если он не просрочен (т.е. не является висячим), обратиться к объекту, на который
он указывает. Это проще сказать, чем сделать. Поскольку у указателей s t d : : wea k_pt r нет
операций разыменования, нет и способа написать такой код. Даже если бы он был, разде­
ление проверки и разыменования могло бы привести к состоянию гонки: между вызовом
expired и разыменованием другой поток мог бы переприсвоить или уничтожить послед­
ний std : : sha red_ptr, указывающий на объект, тем самым приводя к уничтожению само­
го объекта. В этом случае ваше разыменование привело бы к неопределенному поведению.
Что вам нужно - так это атомарная операция, которая проверяла бы просрочен­
ность указателя s t d : : wea k_pt r и, если он не просрочен, предоставляла вам доступ
к указываемому объекту. Это делается путем создания указателя s td : : shared_pt r из
указателя std : : wea k_pt r. Операция имеет две разновидности, в зависимости от того,
что должно произойти в ситуации, когда s t d : : weak_pt r оказывается просроченным
при попытке создания из него s t d : : s hared_pt r. Одной разновидностью является
s t d : : weak_pt r : : l oc k, которая возвращает s t d : : shared_pt r. Этот указатель нулевой,
если std: : weak_pt r просрочен:
std : : shared_ptr spwl
= wpw . lock ( ) ;
auto spw2 = wpw . lock ( ) ;

/ / Если wpw просрочен,
11 spwl
нулевой
11 То же самое, но с auto
-

Второй разновидностью является конструктор s t d : : shared p t r, принимающий
std : : weak_pt r в качестве аргумента. В этом случае, если s t d : : weak_pt r просрочен, ге­
нерируется исключение:
std: : shared_J>tr spwЗ ( wpw) ; / / Если wpw просрочен, гене­
// рируется std : : bad_weak_ptr

Но вас, вероятно, интересует, зачем вообще нужен s t d : : weak_ptr. Рассмотрим фа­
бричную функцию, которая производит интеллектуальные указатели на объекты только
для чтения на основе уникальных значений идентификаторов. В соответствии с советом
из раздела 4. 1 , касающегося возвращаемых типов фабричных функций, она возвращает
std : : unique_ptr:
std : : unique_ptr loadWidget (WidgetID id) ;

Если loadWidget является дорогостоящим вызовом (например, из-за файловых опера­
ций ввода-вывода или обращения к базе данных), а идентификаторы часто используются
4.3. Испопьзуйте std::weak_ptr для std::shared_ptr-noдoбныx указателей, которые моrут быть висячими

1 43

повторно, разумной оптимизацией будет написание функции, которая делает то же, что
и l oadW idget, но при этом кеширует результаты. Засорение кеша всеми затребованными
Widget может само по себе привести к проблемам производительности, так что другой
разумной оптимизацией является удаление кешированных Widget, когда они больше не
используются.
Для такой кеширующей фабричной функции возвращаемый тип s t d : : unique_ptr
не является удачным выбором. Вызывающий код, определенно, получает интеллек­
туальные указатели на кешированные объекты, и время жизни полученных объектов
также определяется вызывающим кодом. Однако кеш также должен содержать указате­
ли на эти же объекты. Указатели кеша должны иметь возможность обнаруживать свое
висячее состояние, поскольку когда клиенты фабрики заканчивают работу с объектом,
возвращенным ею, этот объект уничтожается, и соответствующая запись кеша стано­
вится висячей. Следовательно, кешированные указатели должны представлять собой
указатели s t d : : weak_pt r, которые могут обнаруживать, что стали висячими. Это озна­
чает, что возвращаемым типом фабрики должен быть s t d : : shared_pt r, так как указате­
ли std: : weak_pt r могут обнаруживать, что стали висячими, только когда время жизни
объектов управляется указателями std : : shared_ptr.
Вот как выглядит быстрая и неаккуратная реализация кеширующей версии
loadWidget:
std: : shared_ptr fastLoadWidget (Widget ID id)
stati c std : : unordered_map cache ;
auto obj Ptr
cache [ id] . lock ( ) ;
=

11
11
11
11
11
11

obj Ptr является std: : shared_ptr
для кешированного объекта и
нулевым указателем для объекта ,
отсутствующего в кеше
При отсутствии в кеше
объект загружается

i f ( ! obj Ptr) {
obj Ptr
loadWidget ( id) ;
1 1 и кешируется
cache [ id] = obj Ptr;
=

return obj Ptr;
Эта реализация использует один из контейнеров С++ 1 1, представляющий собой хеш­
таблицу ( std : : unordered_map}, хотя здесь и не показаны хеширование Widge t I D и функ­
ции сравнения, которые также должны присутствовать в коде.
Реализация fastLoadW i dget игнорирует тот факт, что кеш может накапливать про­
сроченные указатели s t d : : wea k_ptr, соответствующие объектам Widget, которые боль­
ше не используются (а значит, были уничтожены). Реализация может быть улучшена,
но вместо того чтобы тратить время на вопрос, который не привнесет ничего нового
в понимание интеллектуальных указателей std : : weak_pt r,давайте рассмотрим второе
1 44

Глава 4. Интеллектуальные указатели

применение этих указателей: шаблон проектирования Observer (Наблюдатель). Основ­
ными компонентами этого шаблона являются субъекты (объекты, которые могут из­
меняться) и наблюдатели (объекты, уведомляемые при изменении состояний). В боль­
шинстве реализаций каждый субъект содержит член-данные, хранящие указатели на его
наблюдателей. Это упрощает для субъектов проблемы уведомления об изменении состо­
яний. Субъекты не заинтересованы в управлении временем жизни своих наблюдателей
(т.е. тем, когда они должны быть уничтожены), но они очень заинтересованы в том, что­
бы, если наблюдатель был уничтожен, субъекты не пытались к нему обратиться. Разум­
ным проектом может быть следующий - каждый субъект хранит контейнер указателей
std : : weak_pt r на своих наблюдателей, тем самым позволяя субъекту определять, не яв­
ляется ли указатель висящим, перед тем как его использовать.
В качестве последнего примера применения s td : : wea k_pt r рассмотрим структуру
данных с объектами А, В и С в ней, где А и С совместно владеют В, а следовательно, хранят
указатели std : : shared_ptr на нее:

Г7l std : : shared ptr

std : : shared ptr

Гг11

r;1
i2.Ji--------�L!.J
�---------�,
-Предположим, что было бы также полезно иметь указатель из в на А. Какую разновид­
ность интеллектуального указателя следует использовать в этом случае?

Есть три варианта.


Обычный указатель. При таком подходе, если уничтожается А, а с продолжает
указывать на В, В будет содержать указатель на А, который становится висящим. В
не в состоянии этого определить, а потому В может непреднамеренно этот указа­
тель разыменовать. В результате получается неопределенное поведение.



Указатель std: : shared_ytr.



Это позволяет избежать обеих описанных выше про­
блем. Если уничтожается А, указатель в в становится висящим, но В в состоянии
это обнаружить. Кроме того, хотя А и В указывают друг на друга, указатель в В не
влияет на счетчик ссылок А, а следовательно, не может предотвратить удаление А,
когда на него больше не указывает ни один std : : shared_pt r.

В этом случае А и В содержат указатели std: : shared_pt r
один на другой. Получающийся цикл std: : shared_ptr (А указывает на В, а В указыва­
ет на А) предохраняет и А, и В от уничтожения. Даже если А и В недостижимы из дру­
гих структур данных программы (например, поскольку С больше не указывает на В),
счетчик ссылок каждого из них равен единице. Если такое происходит, А и В оказы­
ваются потерянными для всех практических применений: программа не в состоянии
к ним обратиться, а их ресурсы не моrут быть освобождены.

Указатель std : : weak_ytr.

4.3. Испоnьэуйте std::weak_ptr дnя std::shared_ptr-noдoбныx указателей, которые могут быть висячими

1 45

Очевидно, что наилучшим выбором является std : : weak_pt r. Однако стоит отметить,
что необходимость применения указателей std : : weak_ptr для предотвращения потен­
циальных циклов из указателей s t d : : shared_pt r не является очень распространенным
явлением. В строго иерархических структурах данных, таких как деревья, дочерними
узлами обычно владеют их родительские узлы. При уничтожении родительского узла
должны уничтожаться и его дочерние узлы. В общем случае связи от родительских к до­
черним узлам лучше представлять указателями s t d : : unique _pt r. Обратные связи от до­
черних узлов к родительским можно безопасно реализовывать, как обычные указатели,
поскольку дочерний узел никогда не должен иметь время жизни, большее, чем время
жизни его родительского узла. Таким образом, отсутствует риск того, что дочерний узел
разыменует висячий родительский указатель.
Конечно, не все структуры данных на основе указателей строго иерархичны, и ког­
да приходится сталкиваться с такими неиерархичными ситуациями, как и с ситуациями
наподобие кеширования или реализации списков наблюдателей, знайте, что у вас есть
такой инструмент, как std : : weak_pt r.
С точки зрения эффективности std : : wea k_ptr, по сути, такой же, как и std : : shared_
ptr. Объекты s t d : : weak_pt r имеют тот же размер, •по и объекты std: : shared_ptr, они
используют те же управляющие блоки, что и указатели s t d : : shared_pt r (см. раздел 4.2),
а операции, такие как создание, уничтожение и присваивание, включают атомарную ра­
боту со счетчиком ссылок. Вероятно, это вас удивит, поскольку в начале этого раздела
я писал, что указатели s t d : : weak_ptr не участвуют в подсчете ссылок. Но это не совсем
то, что я написал. Я написал, что указатели s t d : : weak_pt r не участвуют в совместном
вла д ении объектами, а следовательно, не влияют на счетчик ссылок указываемого о бъ­
екта. На самом деле в управляющем блоке имеется второй счетчик ссылок, и именно
с ним и работают указатели s t d : : weak_ptr. Более подробно этот вопрос рассматривает­
ся в разделе 4.4.
_

Следует запомнить


Используйте std : : weak_pt r как std : : shared_pt r-oбpaзныe указатели, которые
могут быть висячими.



Потенциальные применения std : : wea k_pt r включают хеширование, списки на­
блюдателей и предупреждение циклов указателей s t d : : shared_pt r.

4.4. П редпочитайте испоnьзование s td : : make_unique
и s td : : make_shared непосредственному
испоnьзованию оператора new
Начнем с выравнивания игровой площадки для игры s t d : : ma ke_un i que и s td : :
make_shared против обычных указателей. Функция std : : ma ke_shared является частью
C++ l l , но, увы, s t d : : ma ke_unique таковой не является. Она вошла в стандарт только

1 46

Глава 4. И нтеллектуальные указатели

начиная с С++ 14. Если вы используете С++ 1 1 , не переживайте, потому что базовую вер­
сию s t d : : ma ke _unique легко написать самостоятельно. Смотрите сами:
template
std : : unique_ptr make_unique (Ts&& . . . params )
return std: : unique_pt r (
new T ( st d : : forward (params ) . . . ) ) ;

Как вы можете видеть, ma ke_unique просто выполняет прямую передачу своих пара­
метров в конструктор создаваемого объекта, создает s t d : : unique_pt r из обычного ука­
зателя, возвращаемого оператором new, и возвращает этот указатель st d : : unique_pt r.
Функция в данном виде не поддерживает массивы или пользовательские удалители (см.
раздел 4. 1 ), но зато демонстрирует, как с минимальными усилиями можно при необхо­
димости создать make_unique'. Только не помещайте вашу версию в пространство имен
s t d, поскольку иначе вы можете столкнуться с коллизией имен при обновлении реализа­
ции стандартной библиотеки до С++ 14.
Функции s t d : : make_unique и s t d : : ma ke_shared представляют собой две из трех
mа kе-функций, т.е. функций, которые принимают произвольное количество аргумен­
тов, выполняют их прямую передачу конструктору объекта, создаваемого в динамиче­
ской памяти, и возвращают интеллектуальный указатель на этот объект. Третья mаkе­
функция s t d : : a llocate_shared. Она работает почти так же, как и s t d : : ma ke_shared,
за исключением того, что первым аргументом является объект распределителя, использу­
ющийся для выделения динамической памяти.
Даже самое тривиальное сравнение создания интеллектуального указателя с помо­
щью mаkе-функции и без участия таковой показывает первую причину, по которой при­
менение таких функций является предпочтительным. Рассмотрим следующий код.
-

auto upwl ( std : : make_unique ( ) ) ;
11 С mаkе - функцией
std : : unique_pt r upw2 ( new Widget) ; / / Без mаkе - функции
auto spwl ( std : : make_shared ( ) ) ;
/! С mаkе -функцией
std : : shared_ptr spw2 ( new Widget) ; / / Без mаkе - функции

Я подчеркнул важное отличие: версия с применением оператора new повторяет соз­
данный тип, в то время как mа kе-функции этого не делают. Повторение типа идет враз­
рез с основным принципом разработки программного обеспечения: избегать дублиро­
вания кода. Дублирование в исходном тексте увеличивает время компиляции, может
вести к раздутому объектному коду и в общем случае приводит к коду, с которым слож­
но работать. Зачастую это ведет к несогласованному коду, а несогласованности в коде
часто ведут к ошибкам. Кроме того, чтобы набрать на клавиатуре что-то дважды, надо
3

Дня создания полнофункциональной версии make unique с минимальными усилиями поищите
документ, ставший ее источником, и скопируйте из него ее реализацию. Этот документ N3656
от 18 апреля 20 1 3 года, его автор - Стивен Т. Лававей (Stephan Т. Lavavej).
_

-

4.4. Предпочитайте использование std::make_unique и std::make_shared ""

1 47

затратить в два раза больше усилий, чем для единственного набора, а кто из нас не любит
сократить эту работу?
Второй причиной для предпочтения mа kе-функций является безопасность исключе­
ний. Предположим, что у нас есть функция для обработки Widget в соответствии с не­
которым приоритетом:
void processWidget ( std: : shared_ptr spw, int priori ty ) ;

Передача std : : sha red_pt r по значению может выглядеть подозрительно, но в разде­
ле 8. 1 поясняется, что если processWidget всегда делает копию s t d : : shared_ptr (напри­
мер, сохраняя ее в структуре данных, отслеживающей обработанные объекты Widget ) , то
это может быть разумным выбором.
Предположим теперь, что у нас есть функция для вычисления приоритета
int computePriority ( ) ;
и мы используем ее в вызове proces sWidget, который использует оператор new вместо
std : : make shared:
proces sWidget (
std : : shared_ptr ( new Widge t ) ,
computePriorit y ( ) ) ;

/ / Потенциальная
/ / утечка
/ / ресурса

Как указывает комментарий, этот код может приводить к утечке Widget, вызванной
применением new. Но почему? И вызывающий код, и вызываемая функция используют
указатели s td : : shared_pt r, а s t d : : shared_pt r спроектированы для предотвращения
утечек ресурсов. Они автоматически уничтожают то, на что указывают, когда исчезает
последний std : : shared_ptr. Если все везде используют указатели std : : shared_ptr,
о какой утечке может идти речь?
Ответ связан с тем, как компиляторы транслируют исходный код в объектный. Во
время выполнения аргументы функции должны быть вычислены до вызова функции,
так что в вызове processWidget до того, как processWidget начнет свою работу, должно
произойти следующее.


Выражение new W idget должно быть вычислено, т.е. в динамической памяти дол­
жен быть создан объект W idget.



Должен быть вызван конструктор s t d : : shared _pt r, отвечающий за
управление указателем, сгенерированным оператором new.



Должна быть вызвана функция computePriority.

Компиляторы не обязаны генерировать код, выполняющий перечисленные действия
именно в таком порядке. Выражение new W idget должно быть выполнено до вызова
конструктора std : : shared_pt r, поскольку результат этого оператора new используется
в качестве аргумента конструктора, но функция computePriori ty может быть выполнена
до указанных вызовов, после них или, что критично, между ними, т.е. компиляторы мо­
гут генерировать код для выполнения операций в следующем порядке.

1 48

Глава 4 . Интеллектуальные указатели

1 . Выполнить new Widget.
2. Выполнить computePriority.
3. Вызвать конструктор std : : shared_pt r.
Если сгенерирован такой код и во время выполнения computePriority генерирует ис­
ключение, созданный на первом шаге в динамической памяти Widget будет потерян, так
как он не будет сохранен в указателе s t d : : shared_pt r, который, как предполагается,
начнет управлять им на третьем шаге.
Применение std : : ma ke_shared позволяет избежать таких проблем. Вызывающий код
имеет следующий вид:
processWidget ( std: : make_shared ( ) , / / Потенциальной
compute Priority ( ) ) ;
1 1 утечки ресурсов нет

Во время выполнения либо s t d : : ma ke s ha red, либо compu t e P r i o r i t y будет вы­
звана первой. Если это s t d : : ma ke_sha red, обычный указатель на созданный в ди­
намической памяти W idget будет безопасно сохранен в возвращаемом указателе
std : : shared_pt r до того, как будет вызвана функция compute P ri o r i t y. Если после
этого функция computePriority сгенерирует исключение, деструктор std : : sha red_ptr
уничтожит объект W idget, которым владеет. А если первой будет вызвана функция
computePriority и сгенерирует при этом исключение, то std: : ma ke_shared даже не бу­
дет вызвана, так что не будет создан объект Widget, и беспокоиться будет не о чем.
Если мы заменим std : : shared_pt r и std : : make_shared указателем std : : unique_ptr
и функцией std: : make_unique, все приведенные рассуждения останутся в силе. Исполь­
зование std : : make unique вместо new так же важно для написания безопасного с точки
зрения исключений кода, как и применение s t d : : ma ke_ shared.
Особенностью std : : make_ shared (по сравнению с непосредственным использовани­
ем new) является повышенная эффективность. Применение std: : make_shared позволяет
компиляторам генерировать меньший по размеру и более быстрый код, использующий
более компактные структуры данных. Рассмотрим следующее непосредственное приме­
нение оператора new:
std: : shared_ptr spw ( new Widget ) ;

Очевидно, что этот код предполагает выделение памяти, но фактически он выполняет
два выделения. В разделе 4.2 поясняется, что каждый указатель std : : shared_pt r ука­
зывает на управляющий блок, содержащий, среди прочего, значение счетчика ссылок
для указываемого объекта. Память для этого блока управления выделяется в конструкто­
ре std : : shared_pt r. Непосредственное применение оператора new, таким образом, тре­
бует одного выделения памяти для Widget и второго - для управляющего блока.
Если вместо этого использовать std: : ma ke shared,
auto spw

=

std : : ma ke_shared ( ) ;

то окажется достаточно одного в ыделения памяти. Дело в том, что функция
s t d : : ma ke _ shared выделяет один блок памяти для хранения как объекта Widget, так
4.4. Предпочитайте испоnьзование std::make_unique и std::make_shared....

1 49

и управляющего блока. Такая оптимизация снижает статический размер программы, по­
скольку код содержит только один вызов распределения памяти и повышает скорость
работы выполнимого кода, так как выполняется только одно выделение памяти. Кроме
того, применение std : : make_shared устраняет необходимость в некоторой учетной ин­
формации в управляющем блоке, потенциально уменьшая общий объем памяти, требу­
ющийся для программы.
Анализ эффективности функции s t d : : ma ke_shared в равной мере применим и к
std : : а l loca t e shared, так что преимущество повышения производительности функ­
ции std : : ma ke_shared распространяется и на нее.
Аргументы в пользу предпочтения mаkе-функций непосредственному использованию
оператора new весьма существенны. Тем не менее, несмотря на их проектные преиму­
щества, безопасность исключений и повышенную производительность, данный раздел
говорит о пред почтительном применении mа kе-функций, но не об их исключительном
использовании. Дело в том, что существуют ситуации, когда эти функции не могут или
не должны использоваться.
Например, ни одна из mаkе-функций не позволяет указывать пользовательские
удалители (см. разделы 4. 1 и 4.2), в то время как конструкторы s t d : : uni que _pt r
и std : : shared_pt r это позволяют. Для данного пользовательского удалителя Widget
auto widgetDeleter

=

[ ] ( Widget* pw) {

_

);

создание интеллектуального указателя с применением оператора new является очень
простым:
std : : unique ptr
upw (new Widget, widgetDeleter) ;
std : : shared_ptr spw (new Widget, widgetDeleter ) ;
Сделать то же самое с помощью mаkе-функции невозможно.
Второе ограничение на mаkе-функции проистекает из синтаксических деталей их
реализации. В разделе 3 . 1 поясняется, что при создании объекта, тип которого пере­
гружает конструкторы как с параметрами s t d : : i n i t i a l i ze r_l i s t , так и без них,
создание объекта с использованием фигурных скобок предпочитает конструктор
s td : : i n i t i a l i zer_l i s t , в то время как создание объекта с использованием круглых
скобок вызывает конструктор, у которого нет параметров std : : ini t i a l i zer_ l i st. mаkе­
функции выполняют прямую передачу своих параметров конструктору объекта, но де­
лается ли это с помощью круглых или фигурных скобок? Для некоторых типов ответ
на этот вопрос очень важен. Например, в вызовах
auto upv
auto spv

std : :make_unique ( l O, 2 0 ) ;
std : :make_shared ( l O, 2 0 ) ;

результирующие интеллектуальные указатели должны указывать на векторы std : : vector
с 10 элементами, значение каждого из которых - 20, или на векторы с двумя элементами,
один со значением 10, а второй со значением 20? Или результат непредсказуем?

1 50

Глава 4. Интеллектуальные указатели

Хорошая новость в том, что результат все же предсказуем: оба вызова создают векто­
ры std : : vector с 10 элементами, значение каждого из которых равно 20. Это означает
что в mаkе-функциях прямая передача использует круглые, а не фигурные скобки. Плохая
новость в том, что если вы хотите создавать свои указываемые объекты с помощью ини­
циализаторов в фигурных скобках, то должны использовать оператор new непосредствен­
но. Использование mаkе-функции требует способности прямой передачи инициализатора
в фигурных скобках, но, как поясняется в разделе 5.8, такие инициализаторы не могут быть
переданы прямо. Однако в разделе 5.8 описывается и обходной путь: использование вывода
типа auto для создания объекта std : : ini t ia l i zer_ l is t из инициализатора в фигурных
скобках (см. раздел 1 .2) с последующей передачей созданного объекта через mаkе-функцию:
11

Создание std: : initiali zer_list

auto initList = { 10 , 20 } ;
11

Создание std : : vector с помощью конструктора
с параметром std : : initial i zer_l ist
auto spv
std : : ma ke_sharedtr pimpl; // std: : shared_ptr
// вместо std : : unique_ptr

и приведенном далее коде клиента, который включает заголовочный файл widget . h
Widget wl ;
auto w2 ( std : :move (wl ) ) ; 11 Перемещающее конструирование w2
11 Перемещающее присваивание wl
wl = std: :move (w2 ) ;
все компилировалось бы и работало именно так, как мы рассчитывали: wl был бы создан
конструктором по умолчанию, его значение было бы перемещено в w2, а затем это значе­
ние, в свою очередь, было бы перемещено в wl, после чего и wl, и w2 были бы уничтоже­
ны (тем самым приводя к уничтожению объекта Widget : : Impl ) .
Различие в поведении указателей s td : : unique _pt r и std : : shared_pt r для pimpl
вытекает из различий путей, которыми эти интеллектуальные указатели поддерживают

1 62

Глава 4. Интеллектуальные указатели

пользовательские удалители. Для std : : unique_pt r тип удалителя является частью типа
интеллектуального указателя, и это позволяет компилятору генерировать меньшие
структуры данных времени выполнения и более быстрый код. Следствием этой более
высокой эффективности является то, что указываемые типы должны быть полными, ког­
да используются специальные функции-члены, генерируемые компиляторами (например,
деструкторы или перемещающие операции). В случае s t d : : shared_pt r тип удалителя не
является частью типа интеллектуального указателя. Это требует больших структур дан­
ных времени выполнения и несколько более медленного кода, но зато указываемые типы
не обязаны быть полными при применении специальных функций-членов, генерируемых
компиляторами.
При применении идиомы Pimpl в действительности нет никакого компромисса между
характеристиками s t d : : un i que_p t r и std : : shared_pt r, поскольку отношения между
классами наподобие W idget и Widget : : Impl представляют собой исключительное владе­
ние, и это делает единственно верным выбором в качестве инструмента интеллектуаль­
ный указатель std : : unique_pt r. Тем не менее стоит знать, что в других ситуациях - си­
туациях, в которых осуществляется совместное владение (а следовательно, правильным
выбором является std : : shared_pt r},
нет необходимости прыгать через горящие об­
ручи определений функций, которую влечет за собой применение std : : unique_pt r.
-

Сnедует запомнить


Идиома Pimpl уменьшает время построения приложения, снижая зависимости
компиляции между клиентами и реализациями классов.



Для указателей plmpl типа s t d : : unique_pt r следует объявлять специальные функ­
ции-члены в заголовочном файле, но реализовывать их в файле реализации. Посту­
пайте так, даже если реализации функций по умолчанию являются приемлемыми.



Приведенный выше совет применим к интеллектуальному указателю std : : unique_
pt r, но не к std : : shared_pt r.

4.5. При использовании идиомы указателя на реализацию определяйте специальные" "

1 63

ГЛАВА S

Rvalue - cc ы n к и , с емант и ка
п ереме щ ен и и и п р ямая п ередача
v

На первый взгляд, семантика перемещения и прямой передачи кажется довольно
простой.


Семантика перемещения



Прямая передача

позволяет компиляторам заменять дорогостоящие опера­
ции копирования менее дорогими перемещениями. Так же, как копирующие кон­
структоры и копирующие операторы присваивания дают вам контроль над тем, что
означает копирование объектов, так и перемещающие конструкторы и перемеща­
ющие операторы присваивания предоставляют контроль над семантикой переме­
щения. Семантика перемещения позволяет также создавать типы, которые могут
только перемещаться, такие как std : : unique_pt r, std : : future или s t d : : thread.
делает возможным написание шаблонов функций, которые при­
нимают произвольные аргументы и передают их другим функциям так, что целевые
функции получают в точности те же аргументы, что и переданные исходным функ­
циям.

Rvаluе-ссылки представляют собой тот клей, который соединяет две эти довольно
разные возможности. Это базовый механизм языка программирования, который делает
возможными как семантику перемещения, так и прямую передачу.
С ростом опыта работы с этими возможностями вы все больше понимаете, что ваше
первоначальное впечатление было основано только на пресловутой вершине айсберга.
Мир семантики перемещения, прямой передачи и rvalue-ccылoк имеет больше нюансов,
чем кажется на первый взгляд. Например, std : : move ничего не перемещает, а прямая
передача оказывается не совсем прямой. Перемещающие операции не всеrда дешевле ко­
пирования, а когда и дешевле, то не всегда настолько, как вы думаете; кроме того, они
не всегда вызываются в контексте, где перемещение является корректным. Конструкция
type & & не всегда представляет rvalue-ccылкy.
Независимо от того, как глубоко вы закопались в эти возможности, может показать­
ся, что можно долго копать еще глубже. К счастью, эта глубина не безгранична. Эта глава
доведет вас до коренной породы. Когда вы докопаетесь до нее, эта часть С++ 1 1 будет

выглядеть намного более осмысленной. Например, вы познакомитесь с соглашения­
ми по использованию s t d : : move и std : : forward. Вы почувствуете себя намного более
комфортно, сталкиваясь с неоднозначной природой t ype & & . Вы поймете причины уди­
вительно разнообразного поведения перемещающих операций. Все фрагменты мозаики
встанут на свои места. В этот момент вы окажетесь там, откуда начинали, потому что
семантика перемещений, прямая передача и rvаluе-ссылки вновь покажутся вам доста­
точно простыми. Но на этот раз они будут оставаться для вас такими навсегда.
В этой главе особенно важно всегда иметь в виду, что параметр всегда является lvalue,
даже если ero тип - rvalue-ccылкa. Иными словами, в фрагменте
void f (Wiclget&& w ) ;

параметр w представляет собой lvalue, несмотря на то что его тип - rvalue-ccылкa
на Widget. (Если это вас удивляет, вернитесь к обзору lvalue и rvalue, который содержит­
ся во введении.)

S.1 . Азы s td : : move и s td : : forward
Полезно подойти к s t d : : move и std : : forward с точки зрения того, чего они не дела­
ют. std : : move ничего не перемещает. std : : forward ничего не передает. Во время вы­
полнения они не делают вообще ничего. Они не генерируют выполнимый код - ни од­
ного байта.
std : : move и std : : forward являются всего лишь функциями (на самом деле - шаб­
лонами функций), которые выполняют приведения. std : : move выполняет безусловное
приведение своего аргумента к rvalue, в то время как std : : forward выполняет приведе­
ние только при соблюдении определенных условий. Это все. Пояснения приводят к ново­
му множеству вопросов, но, по сути, история на этом завершена.
Чтобы сделать историю более конкретной, рассмотрим пример реализации std : : move
в С++ 1 1 . Она не полностью соответствует деталям стандарта, но очень близка к этому.
template
11 В пространстве имен std
typename remove_reference : : type & &
m.ove ( T & & param)
{
us ing ReturnType = 11 Объявление псевдонима ; см. раздел 3 . 3
typename remove_reference : : type & & ;
return static_cast (param) ;

Я выделил здесь две части кода. Одна - имя функции, потому что спецификация воз­
вращаемого типа достаточно запутанна, и я бы не хотел, чтобы вы в ней заблудились.
Вторая - приведение, которое составляет сущность функции. Как вы можете видеть,
std : : move получает ссылку на объект (чтобы быть точным - универсальную ссылку;
см. раздел 5.2) и возвращает ссылку на тот же объект.
Часть & & возвращаемого типа функции предполагает, что s t d : : move возвращает
rvalue-ccылкy, но, как поясняется в разделе 5.6, если тип т является \vаluе-ссылкой, Т & &
1 66

Глава S. Rvаluе-ссылки, семантика перемещений и прямая передача

становится \vа\uе-ссылкой. Чтобы этого не произошло, к Т применяется свойство типа
(см. раздел 3.3) s t d : : remove_reference, тем самым обеспечивая применение " & &" к типу,
не являющемуся ссылкой. Это гарантирует, что s t d : : move действительно возвращает
rvа\uе-ссылку, и это важно, поскольку rvа\uе-ссылки, возвращаемые функциями, явля­
ются rvalue. Таким образом, std : : move приводит свой аргумент к rvalue, и это все, что
она делает.
В качестве небольшого отступления скажем, что s td : : move можно реализовать
в С++ 14 меньшими усилиями. Благодаря выводу возвращаемого типа функции (см. раз­
дел 1 .3) и шаблону псевдонима s t d : : remove_reference_t (см. раздел 3.3) std : : move
можно записать следующим образом:
// С++1 4 ; находится в
template
move ( T & & param) / / пространстве имен std

decltype (auto)
[

using ReturnType ramove_reference_t& & ;
return static_cast (param) ;
=

Легче для восприятия, не так ли?
Поскольку s t d : : move ничего не делает, кроме приведения своего аргумента к rvalue,
были предложения дать этому шаблону другое имя, например rva lue_cas t . Как бы
там ни было, у нас имеется имя std : : move, так что нам важно запомнить, что это имя
std : : move делает и чего не делает. Итак, оно выполняет приведение. И оно ничего не
переносит.
Конечно, rvalue являются кандидатами на перемещение, поэтому применение
std : : move для объекта сообщает компилятору, что объект предназначается для пере­
мещения. Вот почему std : : move имеет такое имя: чтобы легко распознавать объекты,
которые могут быть перемещены.
По правде говоря, rvalue обь1чно являются всего лишь кандидатами для перемещения.
Предположим, что вы пишете класс, представляющий аннотации. Конструктор класса
получает параметр s t d : : st ring, представляющий аннотацию, и копирует значение пара­
метра в член-данные. С учетом информации из раздела 8 . 1 вы объявляете параметр как
передаваемый по значению:
class Annotatioп {
puЫ i c :
explicit Annotation ( std: : strinq text ) ; / / Параметр
};

11 копируемый , так что согласно
/ / разделу 8 . 1 он передается по значению

Но конструктору Annotation требуется только прочесть значение text. Ему не нужно его
модифицировать. В соответствии с освященной веками традицией использования cons t
везде, где только можно, в ы переписываете свое объявление, делая text константой:
class Annotation
puЫic :

5.1 .

Аэы std::move и std::forward

1 67

explicit Annotation ( const std: : s t ring text ) ;

};
Чтобы избежать дорогостоящей операции копирования t ext в член-данные, вы оставля­
ете в силе совет из раздела 8 . 1 и применяете std : : move к text, тем самым получая rvalue:
class Annotation (
puЫ i c :
explicit Annotat ion ( const s t d : : st ring text )
value (std: : move (text) )
11 " Перемещение" text в value ;
11 этот код не делает того ,
( ... }
/ / что от него ожидается !
private :
std : : string va lue;
};

Этот код компилируется. Этот код компонуется. Этот код выполняется. Этот код
устанавливает значение члена-данных va lue равным содержимому строки text. Един­
ственное, что отличает этот код от идеальной реализации ваших намерений, - то, что
text не перемещается в va l ue, а копируется. Конечно, text приводится к rvalue с помо­
щью s t d : : move, но text объявлен как const std : : st r i ng, так что перед приведением
text являлся lvalue типа const s t d : : st r ing, так что результатом приведения является
rvalue типа const std : : s t r i ng, и на протяжении всех этих действий константность со­
храняется.
Рассмотрим, как компиляторы определяют, какой из конструкторов std : : st ring дол­
жен быть вызван. Есть две возможности:
class string
puЬl i c :

1 1 s t d : : st ring в действительности представляет
// собой typedef для std : : basic_string

string ( const string& rhs ) ; / / Копирующий конструктор
string ( string&& rhs ) ;
/ / Перемещающий конструктор
};

В списке инициализации членов конструктора Annotat i on результатом s td : : move
( text ) является rvalue типа const std : : st r i ng. Это rvalue нельзя передать перемеща­

ющему конструктору s t d : : s t r i ng, поскольку перемещающий конструктор получает
rvalue-ccылкy на неконстантную s t d : : s t r ing. Однако это rvalue может быть переда­
но копирующему конструктору. поскольку lvalue-ccылкy на const разрешено связывать
с константным rvalue. Таким образом, инициализация члена использует копирующий
конструктор s t d : : s t r i ng, несмотря на то что text был приведен к rvalue! Такое пове­
дение имеет важное значение для поддержания корректности const. Перемещение зна­
чения из объекта в общем случае модифицирует этот объект, так что язык программи­
рования должен не разрешать передавать константные объекты в функции (такие, как
перемещающие конструкторы), которые могут их модифицировать.
168

Глава 5. Rvа l uе-ссыл ки, семантика перемещений и прямая передача

Из этого примера следует извлечь два урока. Во-первых, не объявляйте объекты как
константные, если хотите иметь возможность выполнять перемещение из н их. Запрос
перемещения к константным объектам молча трансформируется в копирующие опера­
ции. Во-вторых, std : : move не только ничего не перемещает самостоятельно, но даже не
гарантирует, что приведенный этой функцией объект будет иметь право быть переме­
щенным. Единственное, что точно известно о результате применения s t d : : move к объ­
екту, - это то, что он является rvalue.
История с std : : forward подобна истории с std : : move, но тогда как s t d : : move вы­
полняет безоговорочное приведение своего аргумента в rvalue, std : : forward делает это
только при определенных условиях. std : : forward является условным приведением. Что­
бы понять, когда приведение выполняется, а когда нет, вспомним, как обычно использу­
ется std : : forward. Наиболее распространенным сценарием является тот, когда шаблон
функции получает параметр, представляющий собой универсальную ссылку, и который
передается другой функции:
void process ( const Widget& lvalArg) ; 11 Обработка lvalue
void process (Widget&& rvalArg ) ;
1 1 Обработка rvalue
template
void logAndProces s ( T & & param)

11 шаблон, передающий
1 1 param на обработку

auto now =
1 1 Получает текущее время
std : : chrono : : system_clock : : now ( ) ;
makeLogEntry ( "Bызoв ' process ' " , now) ;
process ( std : : forward ( param) ) ;

Рассмотрим два вызова logAndProcess, один с lvalue, а другой - с rvalue:
Widget w ;
logAndProcess (w) ;
1 1 Вызов с lvalue
logAndProcess ( s td : : move ( w ) ) ; 11 Вызов с rvalue

В функции logAndProcess параметр param передается функции process. Функция
process перегружена для lvalue и rvalt1e. Вызывая logAndProces s с lvalue, мы, есте­
ственно, ожидаем, что lvalue будет передано функции process как lvalue, а вызывая
logAndProcess с rvalue, мы ожидаем, что будет вызвана перегрузка process для rvalue.
Однако pa ram, как и все параметры функций, является lvalue. Каждый вызов process
внутри logAndProcess будет, таким образом, вызывать перегрузку process для lvalue. Для
предотвращения такого поведения нам нужен механизм для приведения param к rvalue
тогда и только тогда, когда аргумент, которым инициализируется param аргумент, пе­
реданный logAndProcess,
был rvalue. Именно этим и занимается std : : forward. Вот
почему std : : forward представляет собой условное приведение: эта функция выполняет
приведение к rvalue только тогда, когда ее аргумент инициализирован rvalue.
-

-

5.1 . Азы std::move и std::forward

169

Вы можете удивиться, откуда std : : forward может знать, был ли ее аргумент инициа­
лизирован rvalue? Например, как в приведенном выше коде s t d : : forward может сказать,
был ли param инициализирован с помощью lvalue или rvalue? Краткий ответ заключается
в том, что эта информация кодируется в параметре Т шаблона logAndProcess. Этот пара­
метр передается s t d : : forward, которая восстанавливает закодированную информацию.
Детальное описание того, как работает данный механизм, вы найдете в разделе 5.6.
Учитывая, что и s t d : : move, и s t d : : forward сводятся к приведению и единствен­
ная разница между ними лишь в том, что s t d : : move всегда выполняет приведение, в то
время как s t d : : forward
только иногда, вы можете спросить, не можем ли мы обой­
тись без s t d : : move и просто использовать везде s t d : : f o rward. С чисто технической
точки зрения ответ утвердительный: s t d : : forward может сделать все. Необходимости
в std : : move нет. Конечно, ни одна из этих функций не является действительно необход и­
мой, потому что мы могли бы просто вручную написать требуемое приведение, но, я на­
деюсь, мы сойдемся во мнении, что это будет как минимум некрасиво.
Привлекательными сторонами s t d : : move являются удобство, снижение вероятнос­
ти ошибок и большая ясность. Рассмотрим класс, в котором мы хотели бы отслеживать
количество вызовов перемещающего конструктора. Все, что нам надо, - это счетчик,
объявленный как stat i c, который увеличивался бы при каждом вызове перемещающе­
го конструктора. Полагая, что единственными нестатическими данными класса является
std : : string, вот как выглядит обычный (т.е. использующий std : : move ) способ реализа­
ции перемещающего конструктора:
-

class Widget {
puЬl i c :
Widget (Widget & & rhs )
s ( std: : шove ( rhs . s ) )
{ ++rnoveCtorCa l l s ; 1
private :
static std: : s i ze t moveCtorCa l l s ;
s t d : : st ring s ;
1;

Чтобы реализовать то же поведение с помощью s t d : : forward, код должен был бы вы­
глядеть следующим образом:
class Widget {
puЫic :
Widget ( Widget&& rhs )
/ / Безусловная,
s ( std: : forward ( rhs . s ) ) / / нежела тельная
++rnoveCtorCal l s ; 1
11 реализация
};

Заметим сначала, что s t d : : move требует только аргумент функции ( rhs . s ) , в то
время как s t d : : forward требует как аргумент функции ( rhs . s ) , так и аргумент типа
1 70

Гnава 5. Rvalue-ccыnки, семантика перемещений и прямая передача

шаблона ( std : : string ) . Затем обратим внимание на то, что тип, который мы передаем
s t d : : forward, должен быть не ссылочным, поскольку таково соглашение по кодирова­
нию, что передаваемый аргумент является rvalue (см. раздел 5.6). Вместе это означает,
что std : : move требует меньшего ввода текста по сравнению с s t d : : forward и избавляет
от проблем передачи типа аргумента, указывающего, что передаваемый аргумент являет­
ся rvalue. s t d : : move устраняет также возможность передачи неверного типа (например,
std: : string&, что привело бы к тому, что член-данные s был бы создан с помощью ко­
пирования, а не перемещения).
Что еще более важно, так это то, что использование s t d : : move выполняет безуслов­
ное приведение к rvalue, в то время как использование s t d : : forward означает приведе­
ние к rvalue только ссылок, связанных с rvalue. Это два совершенно различных действия.
Первое из них обычно настраивает перемещение, в то время как второе просто передает
объект другой функции способом, сохраняющим исходную характеристику объекта (lvalue
или rvalue). Поскольку эти действия совершенно различны, наличие двух разных функций
(и разных имен функций) является преимуществом, позволяющим их различать.
Следует запомн ить


std : : move выполняет безусловное приведение к rvalue. Сама по себе эта функция не
перемещает ничего.



std : : forward приводит свой аргумент к rvalue только тогда, когда этот аргумент
связан с rvalue.



Ни std : : move, ни s t d : : forward не выполняют никаких действий времени выпол­
нения.

5 .2. Отnичие универса n ьны х ссыnок от rvalue-ccыno к
Говорят, что истина делает нас свободными, но при соответствующих обстоятельствах
хорошо выбранная ложь может оказаться столь же освобождающей. Этот раздел и есть
такой ложью. Но поскольку мы имеем дело с программным обеспечением, давайте из­
бегать слова "ложь': а вместо него говорить, что данный раздел содержит "абстракцию".
Чтобы объявить rvalue-ccылкy на некоторый тип Т, вы пишете Т & &. Таким образом,
представляется разумным предположить, что если вы видите в исходном тексте Т & & , то
имеете дело с rvаluе-ссылками. Увы, не все так просто.
void f (Widget&& pa ram) ;

1 1 rvalue - ccылкa

Widget&& varl

1 1 rvalue - ccылкa

auto&& var2

=

Widget ( ) ;
varl ;

1 1 Не rvalue - ccылкa

template
void f ( std: : vector&& param) ; // rvalue - ccылкa

5.2.

Отличие универсальных ссылок от rvalue-cc ыл o к

1 71

ternplate
void f ( T&& pararn) ;

1 1 Не rvalue - ccылкa

На самом деле "т & & " имеет два разных значения. Одно из них - конечно, rvа\uе-ссылка.
Такие ссылки ведут себя именно так, как вы ожидаете: они связываются только с rvalue
и их смь1сл заключается в идентификации объектов, которые могут быть перемещены.
Другое значение "Т& & " ли бо rvа\uе-ссылка, ли бо \vа\uе-ссылка. Такие ссылки выгля­
дят в исходном тексте как rvа\uе-ссылки (т.е. "т& &"), но могут вести себя так, как если
бы они были \vа\uе-ссылками (т.е. "т&"). Такая дуальная природа позволяет им быть свя­
занными как с rvalue (подобно rvа\uе-ссылкам), так и с lvalue (подобно \vа\uе-ссылкам).
Кроме того, они могут быть связаны как с константными, так и с неконстантными объек­
тами, как с объектами volat i l e, так и с объектами, не являющимися volat i le, и даже
с объектами, одновременно являющимися и const, и volat i le. Они могут быть связаны
практически со всем. Такие беспрецедентно гибкие ссылки заслуживают собственного
имени, и я называю их универсальными ссылками 1 •
Универсальные ссылки возникают в двух контекстах. Наиболее распространенным
являются параметры шаблона функции, такие как в приведенном выше примере кода:
-

ternplate
void f ( Т&& pararn) ;

// pararn - универсальная ссылка

Вторым контекстом являются объявления auto, включая объявление из приведенного
выше примера кода:
auto&& var2 ; var l ;

/ / var2 - универсальная ссылка

Общее в этих контекстах - наличие вывода типа. В шаблоне f выводится тип pararn,
а в объявлении var2 выводится тип переменной var2. Сравните это с приведенными далее
примерами (также взятыми из приведенного выше примера кода), в которых вывод типа
отсутствует. Если вы видите "т & & " без вывода типа, то вы смотрите на rvа\uе-ссылку:
//
11
Widge t && varl ; Widget ( ) ; / /
11
void f (Widget&& pararn) ;

Вывод типа отсутствует;
pararn - rvalue-ccылкa
Вывод типа отсутствует;
varl - rvalue-ccьшкa

Поскольку универсальные ссылки являются ссылками, они должны быть инициализи­
рованы. Инициализатор универсальной ссылки определяет, какую ссылку она представля­
ет: \vа\uе-ссылку или rvа\uе-ссылку. Если инициализатор представляет собой rvalue, уни­
версальная ссылка соответствует rvа\uе-ссылке. Если же инициализатор является lvalue,
универсальная ссылка соответствует \vа\uе-ссылке. Для универсальных ссылок, которые
являются параметрами функций, инициализатор предоставляется в месте вызова:
ternplate
void f (T&& pararn) ; // pararn является универсальной ссьшкой

1 В разделе 5.3 поясняется, что к универсальным ссылкам почти всегда может применяться
std : : forward, так что, когда эта книга готовилась к печати, некоторые члены сообщества С++
начинали именовать универсапьные ссыпки передаваемыми ссылками.
1 72

Глава 5. Rvаluе-ссылки, семантика леремещений и прямая передача

Widget w;
f (W) ;

/ / В f передается lvalue; тип param // Widge t& ( т . е . lvalue - ccылкa )

f ( std: : move (w) ) ;

/ / В f передается rvalue; тип param 11 Widge t & & ( т . е . rvalue-ccыпкa )

Чтобы ссылка была универсальной, вывод типа необходим, но не достаточен. Вид
объявления ссылки также должен быть корректным, и этот вид достаточно ограничен.
Он должен в точности имеет вид "т & & ': Взглянем еще раз на пример, который мы уже
рассматривали ранее:
template
void f ( std: : vector&& param ) ; / / param - rvalue - ccылкa

Когда вызывается f, тип т выводится (если только вызывающий код явно его не укажет,
но этот крайний случай мы не рассматриваем). Однако объявление типа param не имеет вида
"т& &"; оно представляет собой std : : vector& &. Это исключает возможность для param
быть универсальной ссылкой. Следовательно, param является rvаluе-ссылкой, что ваши ком­
пиляторы с удовольствием подтвердят при попытке передать в функцию f lvalue:
std : : vector< int> v;
f (V) ;
11 Ошибка ! Невозможно связать lvalue
11 с rvаluе - ссылкой

Даже простого наличия квалификатора coпst достаточно для того, чтобы отобрать
у ссылки звание универсальной:
template
void f ( const Т&& param ) ; // param - rvalue - ccылкa

Если вы находитесь в шаблоне и видите параметр функции с типом "т& &': вы можете
решить, что перед вами универсальная ссылка. Но вы не должны этого делать, посколь­
ку размещение в шаблоне не гарантирует наличие вывода типа. Рассмотрим следующую
функцию-член push_back в std : : vector:
template / / Из стандарта С++
class Allocator
class vector {
puЫ i c :
void push_back ( T&& х ) ;

};
Параметр push_back, определенно, имеет верный для универсальной ссылки вид, но
в данном случае вывода типа нет. Дело в том, что push_back не может существовать без
конкретного инстанцированного вектора, частью которого он является; а тип этого ин­
станцирования полностью определяет объявление push_back. Другими словами, код
std : : vector v;
5.2.

Отличие универсальных ссылок от rvalue-cc ыл oк

1 73

приводит к следующему инстанцированию шаблона std : : ve ctor:
class vector {
puЫ i c :
/ / rvalue - ccьmкa
void push_back (Widget&& х ) ;
};

Теперь вы можете ясно видеть, что push_bac k не использует вывода типа. Эта функция
push_back для vector (их две - данная функция перегружена) всегда объявляет па­
раметр типа "rvalue-ccылкa на т ".
В противоположность этому концептуально подобная функция-член ernplace_back
в std : : vector выполняет вывод типа:
template / / / / Из стандарта С++
cla s s vector
puЬl i c :
t emplate
void emplace_back (Args&& . . . a rgs ) ;

};
Здесь параметр типа Args не зависит от параметра типа вектора Т, так что Args дол­
жен выводиться при каждом вызове ernpl ace_back. (Да, в действительности Args пред­
ставляет собой набор параметров, а не параметр типа, но для нашего рассмотрения его
можно рассматривать как параметр типа.)
Тот факт, что параметр типа ernplace back имеет имя Args и является при этом уни­
версальным указателем, только усиливает мое замечание о том, что универсальная ссыл­
ка обязана иметь вид "Т& &': Вы не обязаны использовать в качестве имени т. Например,
приведенный далее шаблон принимает универсальную ссылку, поскольку она имеет пра­
вильный вид ( " t yp e & &" ) , а тип pararn выводится (опять же, исключая крайний случай,
когда вызывающий код явно указывает тип):
_

template
1 1 param является
void someFunc (МyТemplateТype&& param ) ; / / универсальной ссьmкой

Ранее я отмечал, что переменные auto также могут быть универсальными ссылками.
Чтобы быть более точным, переменные, объявленные с типом aut o & & , являются универ­
сальными ссылками, поскольку имеет место вывод типа и они имеют правильный вид
(" Т & & " ) . Универсальные ссылки auto не так распространены, как универсальные ссылки,
используемые в качестве параметров шаблонов функций, но они также время от времени
возникают в C++ l l . Гораздо чаще они возникают в C++ l4, поскольку лямбда-выражения
С++ 1 4 могут объявлять параметры aut o & &. Например, если вы хотите написатьлямбда­
выражение С++ 14 для записи времени, которое занимает вызов произвольной функции,
можете сделать это следующим образом:

1 74

Гnа ва S. Rvalue-ccыnки, семантика перемещений и прямая передача

auto timeFuncinvocation =
[ ] ( auto&& func, auto&& . . . params )

/ / С++ 1 4

Запуск таймера ;

/ / Вызов func
std: : forward ( func) (
std : : forward (params ) . . . / / с params
);
Оста нов таймера и запись време ни ;

)

;

Если ваша реакция на код "std : : forward " внутри лямбда-вы­
ражения - "Что за @#$%?!!'; то, вероятно, вы просто еще не читали раздел 6.3. Не беспо­
койтесь о нем. Главное для нас сейчас в этом лямбда-выражении - объявленные как auto&&
параметры. func является универсальной ссылкой, которая может быть связана с любым
вызываемым объектом, как lvalue, так и rvalue. params представляет собой нуль или несколь­
ко универсальных ссылок (т.е. набор параметров, являющихся универсальными ссылками),
которые могут быть связаны с любым количеством объектов произвольных типов. В резуль­
тате, благодаря универсальным ссылкам auto, лямбда-выражение t imeFuncinvocation в со­
стоянии записать время работы почти любого выполнения функции. (Чтобы разобраться
в разнице между "любого" и "почти любого'; обратитесь к разделу 5.8.)
Имейте в виду, что весь этот раздел - основы универсальных ссылок - является
лож . . . простите, абстракцией. Лежащая в основе истина, известная как свертывание ссы­
лок (reference collapsing), является темой раздела 5.6. Но истина не делает абстракцию
менее полезной. Понимание различий между rvаluе-ссылками и универсальными ссыл­
ками поможет вам читать исходные тексты более точно ("Этот Т & & связывается только
с rvalue или с чем утодно?"), а также избегать неоднозначностей при общении с колле­
гами ("Здесь я использовал универсальную ссылку, а не rvalue-ccылкy. . ."). Оно также
поможет вам в понимании смысла разделов 5.3 и 5.4, которые опираются на указанное
различие. Так что примите эту абстракцию, погрузитесь в нее . . . Так же как законы Нью­
тона (технически не совсем корректные) обычно куда полезнее и проще общей теории
относительности Эйнштейна ("истины"), так и понятие универсальных ссылок обычно
предпочтительнее для работы, чем детальная информация о свертывании ссылок.
Сnедует запомнить


Если параметр шаблона функции имеет тип т & & для выводимого типа Т или если
объект объявлен с использованием aut o & & , то параметр или объект является уни­
версальной ссылкой.



Если вид объявления типа не является в точности t уре & & или если вывод типа не
имеет места, то t уре & & означает rvalue-ccылкy.



Универсальные ссылки соответствуют rvаluе-ссылкам, если они инициализируются
значениями rvalue. Они соответствуют lvаluе-ссылкам, если они инициализируются
значениями lvalue.

5.2.

Отл ичие универсальных ссылок от rvalue-ccылoк

1 75

S.3. Испопьзуйте s td : : move дпя rvalue - ccыno к,
дпя универсапьных ссыпок
а std : : forward
-

Rvаluе-ссылки связываются только с объектами, являющимися кандидатами для пе­
ремещения. Если у вас есть параметр, представляющий собой rvalue-ccылкy, вы знаете,
что связанный с ним объект может быть перемещен:
class Widget {
Widget (Widget&& rhs ) ; / / rhs, определенно, ссьmается на
11 объект, который можно перемещать
};

В этом случае вы захотите передавать подобные объекты друтим функциям таким об­
разом, чтобы разрешить им использовать преимущества "правосторонности". Способ,
которым это можно сделать, - привести параметры, связываемые с такими объектами,
к rvalue. Как поясняется в разделе 5. 1 , std : : move не просто это делает, это та задача,
для которой создана эта функция:
class Widget {
puЫ ic :
// rhs является rvalue-ccьmкoй
Widget (Widge t & & rhs )
: пате (std: : move (rhs . name) ) ,
р (std: : move (rhs .р) )
{
}
".

private :
std : : string name;
std : : shared_ptr р ;

};
С друтой стороны, универсальная ссылка (см. раздел 5.2) может быть связана с объек­
том, который разрешено перемещать. Универсальные ссылки должны быть приведены
к rvalue только тогда, когда они были инициализированы с помощью rvalue. В разделе 5.1
разъясняется, что именно это и делает функция std: : for ward:
class Widget {
puЫ i c :
template
void setName ( T & & newName )
/ / newName является
{ name std: : forward (newName) ; } / / универсальной ссьmкой
=

};

Короче говоря, rvаluе-ссылки при их передаче в друтие функции должны быть безус­
ловно приведены к rvalue (с помощью s t d : : move), так как они всегда связываются с rvalue,
а универсальные ссылки должны приводиться к rvalue при их передаче условно (с помо­
щью s t d : : forward), поскольку они только иногда связываются с rvalue.

1 76

Глава 5. Rv а luе-ссы лки, семантика перемещений и прямая передача

В разделе 5.1 поясняется, что можно добиться верного поведения rvalue-ccылoк и с
помощью s t d : : fo rward, но исходный текст при этом становится многословным, под­
верженным ошибкам и неидиоматичным, так что вы должны избегать применения
std : : forward с rvаluе-ссылкам. Еще худшей является идея применения std : : move к уни­
версальным ссылкам, так как это может привести к неожиданному изменению значений
lvalue (например, локальных переменных):
class Widget {
puЫ i c :
template
void setName ( T&& newName )
{ narne
std: : move (newName) ;

/ / Универсальная ссылка .
11 Компилируется, но это

=

11 очень плохое решение !

private :
std: : string name ;
std: : shared_ptr р ;
};

std : : string getWidgetName ( ) ; / / Фабричная функция
Widget w;
11 n
локальная переменная
auto n = getWidgetName ( ) ;
/ / Перемещение n в w 1
w . se tName ( n ) ;
11 Значение n теперь неизвестно
-

Здесь локальная переменная n передается функции w . s e t N arne . Вызывающий код
можно простить за предположение о том, что эта функция по отношению к n является
операцией чтения. Но поскольку setNarne внутренне использует std : : move для безуслов­
ного приведения своего ссылочного параметра к rvalue, значение n может быть переме­
щено в w . name, и n вернется из вызова setName с неопределенным значением. Этот вид
поведения может привести программиста к отчаянию - если не к прямому насилию.
Можно возразить, что setName не должен был объявлять свой параметр как универ­
сальную ссылку. Такие ссылки не могут быть константными (см. раздел 5.2), но setName,
безусловно, не должен изменять свой параметр. Вы могли бы указать, что если перегру­
зить setName для константных значений lvalue и rvalue, то этой проблемы можно было
бы избежать, например, таким образом:
class Widget {
puЬlic :
void setName ( const std: : string& newName )
{ name = newNarne ; }
void setName (std: : string&& newName)
s t d : : rnove ( newName ) ; }
{ name
=

/ / Устанавливается
// из const lvalue
// Устанавливается
11 из rvalue

};
В данном случае это, безусловно, сработает, но у метода есть и недостатки. Во-первых,
требуется вводить исходный текст большего размера (две функции вместо одного
5.3.

Испоnьзуйте std::move дл я rvalue-cc ыл oк, а std::forward - дл я универсальных ссыпок

1 77

шаблона). Во-вторых, это может быть менее эффективным. Например, рассмотрим сле­
дующее применение setName:
w . setName ( "Adela Nova k" ) ;

При наличии версии setName, принимающей универсальную ссылку, функции setName
будет передан строковый литерал "Adel a Nova k " , в котором он будет передан оператору
присваивания для s t d : : s t r ing внутри w. Таким образом, член-данные name объекта w
будет присвоен непосредственно из строкового литерала; никакого временного объекта
s t d : : s t r i ng создаваться не будет. Однако в случае перегруженных версий setName бу­
дет создан временный объект s t d : : s t r i ng, с которым будет связан параметр функции
set Name, и этот временный объект s t d : : s t r i ng будет перемещен в член-данные объ­
екта w. Таким образом, вызов setN ame повлечет за собой выполнение одного конструк­
тора s t d : : s t r ing (для создания временного объекта), одного перемещающего оператора
присваивания s t d : : st r ing (для перемещения newName в w . name ) и одного деструктора
s t d : : s t ring (для уничтожения временного объекта). Это практически наверняка более
дорогостоящая последовательность операций, чем вызов только одного оператора при­
сваивания s t d : : s t r i ng, принимающего указатель cons t c h a r * . Дополнительная стои­
мость может варьироваться от реализации к реализации, и стоит ли беспокоиться о ней,
зависит от приложения и библиотеки; однако, скорее всего, в ряде случаев замена ша­
блона, получающего универсальную ссылку, парой функций, перегруженных для lvalue­
и rvalue-ccылoк, приведет к дополнительным затратам времени выполнения.
Однако наиболее серьезной проблемой с перегрузкой для lvalue и rvalue является не
объем или идиоматичность исходного кода и не производительность времени выполне­
ния. Это - плохая масштабируемость проекта. Widget : : setName принимает только один
параметр, так что необходимы только две перегрузки. Но для функций, принимающих
большее количество параметров, каждый из которых может быть как lvalue, так и rvalue,
количество перегрузок растет в соответствии с показательной функцией: п параметров
требуют 2" перегрузок. И это еще не самый худший случай. Некоторые функции (на са­
мом деле - шаблоны функций) принимают неограниченное количество параметров, каж­
дый из которых может быть как lvalue, так и rvalue. Типичными представителями таких
функций являются s t d : : ma ke_s h ared и, начиная с С++ 1 4, st d : : ma ke_uni que (см. раз­
дел 4.4). Попробуйте написать объявления их наиболее часто используемых перегрузок:
template
/ / Из стандарта C++ l l
shared_ptr make shared (Args&& . . . args ) ;
template
11 Из стандарта С++ 1 4
unique_ptr make_unique (Args&& . . . args ) ;

Для функций наподобие указанных перегрузка для lvalue и rvalue не является прием­
лемым вариантом: единственным выходом является применение универсальных ссылок.
А внутри таких функций, уверяю вас, к универсальным ссылкам при их передаче другим
функциям следует применять s t d : : forwa rd. Вот то, что вы должны делать.

1 78

Глава S. Rvаl uе-ссылки, семантика перемещений и прямая п ередача

Ну хорошо, обычно должны. В конечном итоге. Но не обязательно изначально. В не­
которых случаях вы захотите использовать привязку объекта к rvalue-ccылкe или уни­
версальной ссылке более одного раза в одной функции, и вы захотите гарантировать,
что перемещения не будет, пока вы явно не укажете его выполнить. В этом случае вы за­
хотите применить std : : move (для rvalue-ccылoк) или std : : forward (для универсальных
ссылок) только к последнему использованию ссылки, например:
template
void setSignText ( T & & text )

11 text - универсальная
11 ссылка

{

/ / Используем text, но не
11 изменяем его
/ / Получение текущего времени
auto now =
std : : chrono : : s ystem_clock : : now ( ) ;
s ignHistory . add ( now,
std: : forward ( text) ) ; / / Условное приведение
11 text к rvalue
sign . setText ( text) ;

Здесь мы хотим гарантировать, что значение text не изменится вызовом s ign . setText,
поскольку мы хотим использовать это значение при вызове s ignHi s tory . add. Следова­
тельно, std : : forward применяется только к последнему использованию универсальной
ссылки.
Для std : : move применяются те же рассуждения (т.е. надо применить s t d : : move
к rvalue-ccылкe только при ее последнем использовании), но важно отметить, что в не­
которых редких случаях вы захотите вызвать std : : move_ if _noexcept вместо std : : move.
Чтобы узнать, когда и почему, обратитесь к разделу 3.8.
Если вы имеете дело с функцией, осуществляющей возврат по значению, и возвраща­
ете объект, привязанный к rvalue-ccылкe или универсальной ссылке, вы захотите приме­
нять std : : move или s t d : : forward при возврате ссылки. Чтобы понять, почему, рассмо­
трим функцию operator+ для сложения двух матриц, где о левой матрице точно извест­
но, что она является rvalue (а следовательно, может повторно использовать свою память
для хранения суммы матриц):
мatrix
//
operator+ (мatrix&& lhs , const
{
lhs += rhs ;
return std: : move (lhs) ; / /
//

Возврат по значению
Matrix& rhs )

Перемещение lhs в
возвращаемое значение

С помощью приведения lhs к rvalue в инструкции return (с помощью s t d : : move )
lhs будет перемещен в местоположение возвращаемого функцией значения. Если опу­
стить вызов std : : move,
Matrix
/ / Ка к и ранее above
operator+ (Matrix&& lhs , const Matrix& rhs )
{

5.3.

Используйте std::move дл я rvalue-ccыnoк, а std::forward - дл я универсальных ссыпок

1 79

lhs += rhs ;
returп lhs;

/ / Копирование lhs в
1 1 возвращаемое значение

то тот факт, что l h s представляет собой lvalue, заставит компиляторы вместо пе­
ремещени я копировать его в местоположение возвращаемого функцией значения.
В предположении, что тип Mat r i x поддерживает перемещающее конструирование,
более эффективное, чем копирующее, применение s t d : : move в инструкции return
дает более эффективный код.
Если тип Mat rix не поддерживает перемещения, приведение его к rvalue не повре­
дит, поскольку rvalue будет просто скопировано копирующим конструктором Mat rix (см.
раздел 5. 1 ). Если Matrix позже будет переделан так, что станет поддерживать перемеще­
ние, operator+ автоматически использует данное преимущество при следующей компи­
ляции. В таком случае ничто не будет потеряно (и возможно, многое будет приобретено)
при применении s t d : : move к rvа\uе-ссылкам, возвращаемым из функций, которые осу­
ществляют возврат по значению.
Для универсальных ссылок и std : : forward ситуация схожа. Рассмотрим шаблон
функции reduceAndCopy, который получает возможно сократимую дробь Fract ion, со­
кращает ее, а затем возвращает копию сокращенной дроби. Если исходный объект пред­
ставляет собой rvalue, его значение должно быть перенесено в возвращаемое значение
(избегая тем самым стоимости создания копии), но если исходный объект - lvalue,
должна быть создана фактическая копия:
template
Fraction

reduceAndCopy ( T & & frac)

1 1 Возврат по значению
// Универсальнал ссылка

{

frac . reduce ( ) ;
return std : : forward ( frac ) ; / / Перемещение rvalue и
11 копирование lvalue в
1 1 возвращаемое значение

Если опустить вызов std : : forward, frac будет в обязательном порядке копироваться
в возвращаемое значение reduceAndCopy.
Некоторые программисты берут приведенную выше информацию и пытаются рас­
пространить ее на ситуации, в которых она неприменима. Они рассуждают следующим
образом: "если использование std : : move для параметра, являющегося rvаluе-ссылкой
и копируемого в возвращаемое значение, превращает копирующий конструктор в пере­
мещающий, то я могу выполнить ту же оптимизацию для возвращаемых мною локаль­
ных переменных". Другими словами, они считают, что если дана функция, возвращающая
локальную переменную по значению, такая, как следующая:
Widget makeWidget ( ) 1 1 "Коnирующал" версил makeWidget
{

Widqet w ;

1 80

1 1 Переменнал
1 1 Настройка w

Глава S. Rvalue-cc ыnки, семантика перемещений и прямая передача

return w ;

11

"Копирование" w в возвращаемое значение

то они могут "оптимизировать" ее, превратив "копирование" в перемещение:
Widget makeWidget ( )

11

Перемещающая версия makeWidget

{

Widget w ;

return std: : move (w) ; / / Перемещение w в возвращаемое
11 значение ( не делайте этого ! )

Мое обильное использование кавычек должно подсказать вам, что эти рассуждения не
лишены недостатков. Но почему? Да потому что Комитет по стандартизации уже прошел
этот путь и давно понял, что "копирующая" версия makeWidget может избежать необхо­
димости копировать локальную переменную w, если будет создавать ее прямо в памяти,
выделенной для возвращаемого значения функции. Это оптимизация, известная как оп­
тимизация возвращаемого значения (retшn value optimization
RVO) и с самого начала
благословленная стандартом С++.
Формулировка такого благословения - сложное дело, поскольку хочется разрешить
такое отсутствие копирования только там, где оно не влияет на наблюдаемое поведение
программы. Перефразируя излишне сухой текст стандарта, это благословение на отсут­
ствие копирования (или перемещения) локального объекта2 в функции, выполняющей
возврат по значению, дается компиляторам, если ( 1 ) тип локального объекта совпадает
с возвращаемым функцией и (2) локальный объект представляет собой возвращаемое
значение. С учетом этого вернемся к "копирующей" версии makeWidget:
-

Widget makeWidget ( ) // " Копирующая" версия makeWidget
{
Widget w ;
return w ;

/ / "Копирование" w в возвращаемое значение

Здесь выполняются оба условия, и вы можете доверять мне, когда я говорю вам, что
каждый приличный компилятор С++ будет использовать RVO для того, чтобы избежать
копирования w. Это означает, что "копирующая" версия ma keWidget на самом деле копи­
рования не выполняет.
Перемещающая версия ma keW idget делает только то, о чем говорит ее имя (в предпо­
ложении наличия перемещающего конструктора Widget ) : она перемещает содержимое w
2

Такими локапьными объектами я1111яются бопьшинство покапьных переменных (например, такие
как w в ma keWidget ) , а также временные объекты, создаваемые как часть инструкции ret urn.
Параметры функции на такое звание претендовать нс могут. Некоторые программисты разпичают
применение RVO к именованным и неимепованным (т.е. временным) локальным объектам, огра­
ничивая термин "RVO" неименованными объектами и назы11ая его применение к именованным
объектам оптимизацией именованных возвращаемых значений (named return value optimization
-

NRVO).

5.3. Используйте std::move дл я rvalue-cc ыл o к, а std::forward - дл я универсальных ссылок

181

в местоположение возвращаемого значения makeWidget . Но почему компиляторы не ис­
пользуют RVO для устранения перемещения, вновь создавая w в памяти, выделенной
для возвращаемого значения функции? Ответ прост: они не могут. Условие (2) предусма­
тривает, что RVO может быть выполнена, только если возвращается локальный объект,
но в перемещающей версии ma keW i d g e t это не так. Посмотрим еще раз на инструкцию
return:
return std: : move (w) ;

То, что здесь возвращается, не является локальным объектом w; это ссьтка на w
ре­
зультат s t d : : move ( w ) . Возврат ссылки на локальный объект не удовлетворяет услови­
ям, требующимся для применения RVO, так что компиляторы вынуждены перемещать w
в местоположение возвращаемого значения функции. Разработчики, пытаясь с помощью
применения s t d : : move к возвращаемой локальной переменной помочь компиляторам
оптимизировать код, на самом деле ограничивают возможности оптимизации, доступ­
ные их компиляторам!
Но RVO
это всего лишь оптимизация. Компиляторы не обязаны устранять опе­
рации копирования и перемещения даже тогда, когда это им позволено. Возможно, вы
параноик и беспокоитесь о том, что ваши компиляторы будут выполнять операции ко­
пирования, просто потому, что они могут это делать. А может, вы настолько глубоко раз­
бираетесь в ситуации, что в состоянии распознать случаи, когда компиляторам трудно
применять RVO, например когда различные пути выполнения в функции возвращают
разные локальные переменные. (Компиляторы должны генерировать код для построения
соответствующей локальной переменной в памяти, выделенной для возвращаемого зна­
чения функции, но как компиляторы смогут определить, какая локальная переменная
должна использоватьсяn В таком случае вы можете быть готовы заплатить цену переме­
щения как гарантию того, что копирование выполнено не будет. Иначе говоря, вы може­
те продолжать думать, что применение s t d : : move к возвращаемому локальному объекту
разумно просто потому, что при этом вы спокойны, зная, что вам не придется платить за
копирование.
В этом случае применение std : : move к локальному объекту все равно остается пло­
хой идеей. Часть стандарта, разрешающая применение RVO, гласит далее, что если ус­
ловия для применения RVO выполнены, но компиляторы предпочитают не выполнять
удаление копирования, то возвращаемый объект должен рассматриваться как rvalue. По
сути, стандарт требует, чтобы, когда оптимизация RVO разрешена, к возвращаемому ло­
кальному объекту либо применялось удаление копирования, либо неявно применялась
функция s t d : : move. Так что в "копирующей" версии makeWidget
-

-

Widget makeWidget ( )

1 1 Как

и

ранее

{

Widget w ;
return w;

182

Гnава 5. Rvalue-ccыnки, семантика перемещений и п рямая передача

компиляторы должны либо устранить копирование w, либо рассматривать функцию, как
если бы она была написана следующим образом:
Widget makeWidget ( )
(
Widget w;
return std: : move(w) ; / / Рассматривает w как rvalue , поскольку
/ / удаление копирования не выполняется

Ситуация аналогична для параметров функции, передаваемых по значению. Они не
имеют права на удаление копирования при их возврате из функции, но компиляторы
должны рассматривать их в случае возврата как rvalue. В результате, если ваш исходный
текст выглядит как
Widget makeWidget (Wiclget w ) / / Передаваемый по значению параметр

// имеет тот же тип, что и
1 1 возвращаемый тип функции

{
return w;

компиляторы должны рассматривать его, как если бы он был написан как
Widget makeWidget (Widget w )
{

return std: : move (w) ;

11 w рассматривается как rvalue

Это означает, что, используя s t d : : move для локального объекта, возвращаемого функ­
цией по значению, вы не можете помочь компилятору (он обязан рассматривать локальный
объект как rvaJue, если не выполняет удаления копирования), но вы, определенно, в со­
стоянии ему помешать (препятствуя RVO). Есть ситуации, когда применение s t d : : move
к локальной переменной может быть разумным (т.е. когда вы передаете ее функции и зна­
ете, что больше вы ее использовать не будете), но эти ситуации не включают применение
s t d : : rnove в качестве части инструкции return, которая в противном случае претендовала
бы на оптимизацию RVO, или возврат параметра, передаваемого по значению.
Сnедует запомнить


Применяйте s t d : : move к rvаluе-ссылкам, а s t d : : forward
кам, когда вы используете их в последний раз.



Делайте то же для rvalue- и универсальных ссылок, возвращаемых из функций
по значению.



Никогда не применяйте s t d : : move и s t d : : forward к локальным объектам, которые
могут быть объектом оптимизации возвращаемого значения.

5.3 . Испол ьзуйте std::move АЛА rvalue-ccы л oк, а std::forward

-

-

к универсальным ссыл­

АЛА универсальных ссы л ок

1 83

S .4. И збе rайте пере r рузок дnя универсаnьных ссыnок
Предположим, что вам надо написать функцию, которая принимает в качестве па­
раметра имя, записывает в журнал текущие дату и время, а затем добавляет имя в гло­
бальную структуру данных. Вы могли бы начать с функции, которая имеет примерно
следующий вид:
std: : multiset пames ; / / Глобальная структура данных
void logAndAdd ( const std: : s tring& пате )
{
auto поw
11 Получение текущего времени
std: : chroпo : : system_clock : : поw ( ) ;
/ / Создание журнальной записи
log ( пow, " logAndAdd " ) ;
пames . emplace (пame ) ;
11 Добавление паmе в глобальную
1 1 структуру даннь� ; emplace
1 1 см. в разделе 8 . 2
=

Этот код не является неразумным, но он не такой эффективный, каким мог бы быть. Рас­
смотрим три потенциальных вызова:
std: : st ring petName ( "Darla" ) ;
11 lvalue типа std : : striпg
logAndAdd (petName) ;
logAndAdd ( std: : strinq ( "Persephone") ) ; // rvalue типа std : : striпg
logAndAdd ( "Patty Doq" ) ;
/ / Строковый литерал

В первом вызове параметр паmе функции 1 ogAпdAdd связывается с перемен­
ной petName. Внутри logAпdAdd параметр паmе в конечном итоге передается в вызов
пames . emplace. Поскольку паmе является lvalue, он копируется в пames. Избежать этого
копирования невозможно, так как lvalue (petName) передается в функцию logAпdAdd.
Во втором вызове параметр name связывается с гvalue (временный объект
std : : st r i пg, явно созданный из строки " Persephoпe " ) . Параметр паmе сам по себе яв­
ляется lvalue, так что он копируется в пames, но мы отдаем себе отчет, что, в принципе,
это значение может быть перемещено в пames. В этом вызове мы платим за копирование,
но мы должны быть способны сделать то же с помощью перемещения.
В третьем вызове параметр паmе опять связывается с rvalue, но в этот раз со времен­
ным объектом s t d : : s t r iпg, который неявно создается из " Patty Dog'' . Как и во вто­
ром вызове, паmе копируется в names, но в этот раз аргумент, изначально переданный
в logAпdAdd, был строковым литералом. Если бы строковый литерал непосредственно
передавался в emplace, в создании временного объекта std : : str i пg не было бы необ­
ходимости вообще. Вместо этого функция emplace использовала бы строковый литерал
для создания объекта std : : str ing непосредственно в std : : mu l t i set. Таким образом,
в этом третьем вызове мы платим за копирование std : : st ri ng, при том что нет причин
платить даже за перемещение, не говоря уже о копировании.
Неэффективность второго и третьего вызовов logAndAdd можно устранить, перепи­
сав эту функцию так, чтобы она принимала универсальную ссылку (см. раздел 5.2) и,
1 84

Глава S . Rvа l uе-ссылки , семантика перемещений и прямая передача

согласно разделу 5.3, передавала ее с помощью s t d : : f o rward функции ernp l a ce . Резуль­
тат говорит сам за себя:
tamplate
void logAndAdd (T&& name )
{
auto now = std : : chrono : : system_clock : : now ( ) ;
log ( now, " logAndAdd " ) ;
names . emplace ( std : : forward (name ) ) ;

std : : string petName ( " Darla " ) ;

11 Как и ранее

logAndAdd (petNaшe) ;

11 Как и ранее , копирова­
// ние lvalue в multiset

logAndAdd (std: : strinq ( "Persephone") ) ; / / Перемещение rvalue
11 вместо копирования
logAndAdd ( "Patty Dog" ) ;

11 Соэдание s td : : string
1 1 в mult i set вместо
/ /копирования временного
11 s t d : : st ring

Ура, получена оптимальная эффективность!
Если бы это был конец истории, мы могли бы остановиться и гордо удалиться, но
я не сказал вам, что клиенты не всегда имеют непосредственный доступ к именам, тре­
бующимся logAndAdd. Некоторые клиенты имеют только индекс, который l ogAndAdd ис­
пользует для поиска соответствующего имени в таблице. Для поддержки таких клиентов
выполняется перегрузка функции logAndAdd:
std : : string nameFromidx ( int idx ) ; / / Возвращает имя,
1 1 соответствующее idx
/ / Новая перегрузка
void logAndAdd ( int iclx)
{
auto now
s t d : : chrono : : system_clock : : now ( ) ;
log ( now, " logAndAdd " ) ;
name s . emplace (nameFroшiclx (iclx) ) ;
=

Разрешение перегрузки работает, как и следовало ожидать:
std: : s tring petName ( " Darla" ) ;

1 1 Как и ранее

11 Как и ранее, эти вызовы
logAndAdd (petName ) ;
logAndAdd ( s td : : string ( " Persephone " ) ) ; 11 приводят к испольэова­
// нию перегрузки для Т&&
logAndAdd ( " Patty Dog" ) ;
logAndAdd ( 2 2 ) ;

1 1 Вызов int -перегруэки
S.4. Избегайте перегрузок дnя ун мверсаnьных ссыпок

1 85

На самом деле разрешение работает, как ожидается, только если вы не ожидаете слиш ком многого. Предположим, клиент имеет переменную типа short , хранящую индекс,
и передает ее функции logAndAdd:
short name idx ;
11 Дает значение переменной name idx
logAndAdd ( namei dx ) ; // Ошибка 1

Комментарий в последней строке, может быть, не слишком понятен, так что позвольте
мне пояснить, что же здесь произошло.
Имеется две перегрузки logAndAdd. Одна из них, принимающая универсальную ссыл­
ку, может вывести тип Т как short, тем самым приводя к точному соответствию. Пере­
грузка с параметром int может соответствовать аргументу short только с повышением.
Согласно обычным правилам разрешения перегрузки точное соответствие побеждает
соответствие с повышением, так что вызывается перегрузка для универсальной ссылки.
В этой перегрузке параметр name связывается с переданным значением типа short.
Таким образом, name передается с помощью s t d : : forward функции-члену emplace объ­
екта name s ( s t d : : mu l t i s e t < s t d : : s t r i n g > ) , которая, в свою очередь, послушно пере­
дает его конструктору s t d : : s t ri ng. Но конструктора s t d : : s t r i ng, который принимал
бы значение short, не существует, так что вызов конструктора s t d : : s t r i ng в вызове
mul t i se t : : emplace в вызове logAndAdd неудачен. Все дело в том, что перегрузка для уни­
версальной ссылки точнее соответствует аргументу типа short, чем перегрузка для int.
Функции, принимающие универсальные ссылки, оказываются самыми жадными в С++.
Они в состоянии выполнить инстанцирование с точным соответствием практически
для любого типа аргумента (несколько видов аргументов, для которых это не так, описа­
ны в разделе 5.8). Именно поэтому сочетание перегрузки и универсальной ссылки почти
всегда является плохой идеей: перегрузка для универсальных ссылок годится для гораздо
большего количества типов аргументов, чем обычно ожидает разработчик перегрузок.
Простой способ свалиться в эту яму - написать конструктор с прямой передачей.
Небольшое изменение функции l ogAndAdd демонстрирует эту проблему. Вместо напи­
сания свободной функции, которая принимает либо s t d : : s t ri ng, либо индекс, который
можно использовать для поиска s t d : : s t ring, представим себе класс Person с конструк­
торами, которые выполняют те же действия:
class Person {
puЫ i c :
template
// Конструктор с прямой передачей
expl icit Person ( T & & n)
: name ( s td : : forward ( n ) ) { } / / инициализирует члены-данные
explicit Person ( int idx )
/ / Конструктор с параметром int
: name ( nameFromidx ( idx ) ) { }
private :
std: : string name ;

};
1 86

Гnава 5. Rvalue-cc ыnки, семантика перемещений и прямая передача

Как и в случае с logAndAdd, передача целочисленного типа, отличного от int (напри­
мер, std : : s i ze_t, short, l ong и т.п.), будет вызывать перегрузку конструктора для уни­
версальной ссылки вместо перегрузки для i nt, и это будет вести к ошибкам компиля­
ции. Однако проблема гораздо хуже, поскольку в Person имеется больше перегрузок,
чем видит глаз. В разделе 3 . 1 1 поясняется, что при соответствующих условиях С++ будет
генерировать как копирующие, так и перемещающие конструкторы, и это так и будет,
даже если класс содержит шаблонный конструктор, который при инстанцировании в со­
стоянии дать сигнатуру копирующего или перемещающего конструктора. Если таким
образом генерируются копирующий и перемещающий конструкторы для Person, класс
Person будет выглядеть, по сути, следующим образом:
class Person [
puЬlic :
/ / Конструктор с прямой передачей
template
explicit Person (T&& n )
: name ( std : : forward ( n ) ) [ )
explicit Person ( int idx) ;

/ / Конструктор от int

Person (const Person& rhs ) ; / / Копирующий конструктор
Person (Person&& rhs) ;

/! ( сгенерирован компилятором)
/ / Перемещающий конструктор
// ( сгенерирован компилятором)

);

Это приводит к поведению, интуитивно понятному, только если вы потратили на ра­
боту с компиляторами и общение с их разработчиками столько времени, что забыли,
каково это - быть человеком:
Person р ( "Nапсу" ) ;
auto cloneOfP ( p ) ; / / Создание нового объекта Person из р ;
1 1 этот код н е компилируется '

Здесь мы пытаемся создать объект Person из другого объекта Person, что представ­
ляется очевидным случаем копирующего конструирования (р является lvalue, так что
можно выбросить из головы все фантазии на тему копирования с помощью операции перемещения). Но этот код не вызывает копирующий конструктор - он вызыва­
ет конструктор с прямой передачей. Затем эта функция будет пытаться инициализиро­
вать член-данные s t d : : s t ring объекта Person значением из объекта Person (р}. Класс
std : : st ring не имеет конструктора, получающего параметр типа Person, так что ваш
компилятор будет вынужден просто развести руками и наказать вас длинными и непо­
нятными сообщениями об ошибках.
"Но почему, - можете удивиться вы, - вызывается конструктор с прямой переда­
чей, а не копирующий конструктор? Мы же инициализируем Person другим объектом
Person!" Да, это так, но компиляторы приносят присягу свято соблюдать правила С++,
а правила, имеющие отношение к данной ситуации, - это правила разрешения вызовов
перегруженных функций.
С(

S. 4.

J)

И збегайте перегрузок для универсальных ссылок

187

Компиляторы рассуждают следующим образом. cloneOf P инициализируется некон­
стантным lvalue (р}, а это означает, что шаблонный конструктор может быть инстанци­
рован для получения неконстантного lvalue типа Person. После такого инстанцирования
класс Person выглядит следующим образом:
class Person {
puЬl ic :
explicit Person (Person& n)

/ / Инстанцирован из

: name ( std : : forward ( n ) ) { } / / шаблона с прямой
1 1 передачей
expl icit Person ( int idx ) ;
Persoп ( const Person& rhs ) ;
)

/ / Как и ранее
/ / Копирующий конструктор
11 ( сгенерирован компилятором)

;

В инструкции
auto cloneOfP ( p ) ;
р

может быть передан либо копирующему конструктору, либо инстанцированному ша­
блону. Вызов копирующего конструктора для точного соответствия типа параметра тре­
бует добавления к р модификатора const; вызов инстанцированного шаблона никаких
добавлений не требует. Таким образом, перегрузка, сгенерированная из шаблона, пред­
ставляет собой лучшее соответствие, так что компиляторы делают то, для чего предна­
значены: генерируют вызов той функции, которая соответствует наилучшим образом.
"Копирование" неконстантных lvalue типа Person, таким образом, осуществляется кон­
структором с прямой передачей, а не копирующим конструктором.
Если мы немного изменим пример, так, чтобы копируемый объект был константным,
то увидим совершенно иную картину:
const Person cp ( "Nancy" ) ; / / Теперь объект константный
auto cloneOfP ( ер ) ;
// Вызов копирующего конструктора !

Поскольку копируемый объект теперь объявлен как const, он в точности соответствует
типу параметра, получаемого копирующим конструктором. Шаблонизированный кон­
структор также может быть инстанцирован таким образом, чтобы иметь ту же сигнатуру:
class Person
puЫic :
explicit Person (const Person& n) ; / / Инстанцирован из шаблона
Person ( const Person& rhs ) ;
/ / Копирующий конструктор
1 1 ( сгенерирован компилятором)
};

Но это не имеет значения, поскольку одно из правил разрешения перегрузок в С++
гласит, что в ситуации, когда инстанцирование шаблона и нешаблонная функция (т.е.
"нормальная" функция) имеют одинаково хорошее соответствие, предпочтение отдается
1 88

Глава S. Rvаluе-ссылки, семантика перемещений и прямая передача

нормальной функции. Поэтому все козыри оказываются на руках копирующего кон­
структора (нормальной функции) с той же самой сигнатурой.
(Если вам интересно, почему компиляторы создают копирующий конструктор, если
они могут инстанцировать шаблонный конструктор с той же сигнатурой, обратитесь
к разделу 3. 1 1 .)
Взаимодействие между конструкторами с прямой передачей и сгенерированными
компилятором операциями копирования и перемещения становится еще более сложным,
когда в картину включается наследование. В частности, обычные реализации копирую­
щих и перемещающих операций производного класса ведут себя совершенно неожидан­
но. Взгляните на следующий код:
class Spec i alPerson : puЫic Person
puЫ ic:
SpecialPerson ( const Specia lPerson& rhs ) / / Копирующий
Person (rhs)
11 конструктор ; вызывает конструктор
11 базового класса с прямой передачей !
{ ". )
SpecialPerson ( SpecialPerson&& rhs )
// Перемещающий
Person (std: : move (rhs) ) / / конструктор ; вызывает конструктор
/ / базового класса с прямой передачей 1
{ . )
"

)

;

Как указывают комментарии, копирующий и перемещающий конструкторы произво­
дного класса не вызывают копирующий и перемещающий конструкторы базового клас­
са; они вызывают конструктор базового класса с прямой передачей! Чтобы понять, по­
чему, обратите внимание, что функции производного класса используют аргументы типа
Spec i a l Person для передачи в базовый класс, после чего в игру вступает разрешение пере­
грузок для конструкторов в классе Person. В конечном итоге код не будет компилировать­
ся, потому что у std : : st ring нет никакого конструктора, принимающего Special Persoп.
Я надеюсь, что теперь я убедил вас, что перегрузка для параметров, являющихся универ­
сальными ссылками, - это то, чего лучше избегать, насколько это возможно. Но если пере­
грузка для универсальной ссылки - плохая идея, то что же делать, если вам нужна функция,
которая выполняет передачу большинства типов аргументов, но при этом должна обраба­
тывать некоторые из них особым образом? Это яйцо может быть разбито массой способов.
Этих способов так много, что я посвятил им целый раздел - раздел 5.5, который идет сразу
после того, который вы сейчас читаете. Не останавливайтесь, и вы попадете прямо в него.
Спедует запомнить


Перегрузка для универсальных ссылок почти всегда приводит к тому, что данная
перегрузка вызывается чаще, чем вы ожидаете.



Особенно проблематичны конструкторы с прямой передачей, поскольку они обычно
соответствуют неконстантным lvalue лучше, чем копирующие конструкторы, и моrут
перехватывать вызовы из производного класса копирующих и перемещающих кон­
структоров базового класса.
S.4. Избегайте перегрузок дnя универсальных ссыпок

1 89

S .S . Знакомство с а nыернативами пере r рузки
дnя универсаnьных ссыпок
В разделе 5.4 поясняется, что перегрузка для универсальных ссылок может приве­
сти к целому ряду проблем как для автономных функций, так и для функций-членов (в
особенности для конструкторов). Тем не менее в нем также приводятся примеры, когда
такая перегрузка может оказаться полезной, если только она будет вести себя так, как
мы· хотим! В этом разделе исследуются способы достижения желаемого поведения либо
путем проектирования, позволяющего избежать перегрузок для универсальных ссылок,
либо путем применения их таким образом, чтобы ограничить типы аргументов, которым
они могут соответствовать.
Ниже использованы примеры, представленные в разделе 5.4. Если вы читали его дав­
но или вовсе не читали, просмотрите указанный раздел, прежде чем читать данный.

Отказ от пе р еr рузк и
В первом примере раздела 5.4 функция logAndAdd является типичным представи­
телем функций, которые могут избежать недостатков перегрузки для универсальных
ссылок, просто используя разные имена для потенциальных перегрузок. Например,
рассматривавшиеся перегрузки l ogAndAdd могут быть разделены на logAndAddName
и logAndAddNameidx. Увы, этот подход не будет работать для второго рассматривавшего­
ся примера - конструктора Person, потому что имена конструкторов в языке зафикси­
рованы. Кроме того, кто же захочет отказаться от перегрузки�

Пе р едача cons t т &
Одной из альтернатив является возврат к С++98 и замена передачи универсальной
ссылки передачей lvаluе-ссылки на const. Фактически это первый подход, рассматривав­
шийся в разделе 5.4. Его недостаток состоит в том, что данный дизайн не столь эффекти­
вен, как нам хотелось бы. С учетом всех нынешних наших знаний о взаимодействии уни­
версальных ссылок и перегрузки отказ от некоторой эффективности в пользу простоты
может оказаться более привлекательными компромиссом, чем нам казалось изначально.

Пе редача по значен и ю
Подход, который часто позволяет добиться производительности без увеличения
сложности, заключается в замене передачи параметров по ссылке передачей по значению,
как бы противоестественно это ни звучало. Этот дизайн основан на совете из раздела 8. 1 :
рассмотреть вопрос о передаче объектов п о значению в случае, когда известно, что они
будут копироваться. Поэтому я отложу до указанного раздела подробное обсуждение
того, как все это работает и насколько эффективным является это решение. Здесь я про­
сто покажу, как этот подход может использоваться в примере с классом Person:
class Person
puЫ i c :

1 90

Глава 5. Rvаluе-ссылки, семантика перемещений и п рямая передача

expl icit Person ( std : : string n ) / /
//
: name ( std : : move ( n ) ) { 1
11
//
explicit Person ( int idx f
: name ( nameFromidx ( idx ) ) 1 1

Замена конструктора с Т & & ;
о применении std: : move
читайте в разделе 8 . 1
Как и ранее

private :
std : : string name ;
f;

Поскольку конструктора s t d : : st r i ng, принимающего только целочисленное значение,
нет, все аргументы типа i nt и подобных ему (например, std : : s i ze_ t, short, l ong), пере­
даваемые конструктору Person, будут перенаправлены к перегрузке для int. Аналогично все
аргументы типа std : : string (а также аргументы, из которых могут быть созданы объекты
std: : string, например литералы наподобие "Ruth" ) будут передаваться конструктору, при­
нимающему std : : string. Здесь для вызывающего кода нет никаких сюрпризов. Возможно,
некоторые программисты будут удивлены, что применение О или NULL в качестве нулевого
указателя приведет к вызову перегрузки для int, но таким программистам необходимо вни­
мательно прочесть раздел 3.2 и читать его до полного просветления в данном вопросе.

Д и сп етчериза ция де с кри п торов
Ни передача lvаluе-ссылки на const, ни передача по значению не предоставляют под­
держку прямой передачи. Если мотивом использования универсальной ссылки является
прямая передача, мы вынуждены использовать универсальную ссылку; у нас просто нет
иного выбора. Тем не менее мы не хотим отказываться и от перегрузки. Если мы не отка­
зываемся ни от перегрузок, ни от универсальных ссылок, то как же мы сможем избежать
перегрузки для универсальных ссылок?
В действительности это не так трудно. Вызовы перегруженных функций разрешаются
путем просмотра всех параметров всех перегрузок, а также всех аргументов в точке вы­
зова с последующим выбором функции с наилучшим общим соответствием - с учетом
всех комбинаций "параметр/аргумент". Параметр, являющийся универсальной ссылкой,
обычно обеспечивает точное соответствие для всего, что бы ни было передано, но если
универсальная ссылка является частью списка параметров, содержащего другие параме­
тры, универсальными ссылками не являющиеся, достаточно плохое соответствие этих
последних параметров может привести к отказу от вызова такой перегрузки. Эта идея ле­
жит в основе подхода диспетчеризации дескрипторов (tag dispatch), а приведенный ниже
пример позволит лучше понять, о чем идет речь.
Применим этот метод к примеру logAndAdd из третьего фрагмента кода раздела 5.4.
Чтобы вам не пришлось его искать, повторим его здесь:
std : : multiset name s ; / / Глобальная структура данных
template
void logAndAdd ( T & & name )

11 Делает запись в журнале и
11 добавляет name в names

5.5. Знакомство с альтернативами перегрузки для универсальных ссылок

191

auto now
std : : chrono : : system_clock: : now ( ) ;
log ( now, " logAndAdd" ) ;
names . emplace ( std : : forward ( name ) ) ;
=

Сама по себе эта функция работает отлично, но если мы добавим перегрузку, принима­
ющую значение типа int, использующееся для поиска объекта по индексу, то получим про­
блемы, описанные в разделе 5.4. Цель данного раздела - их избежать. Вместо добавления
перегрузки мы реализуем l ogAndAdd заново для делегирования работы двум другим функ­
циям: одной - для целочисленных значений, а другой - для всего прочего. Сама функция
logAndAdd будет принимать все типы аргументов, как целочисленные, так и нет.
Эти две функции, выполняющие реальную работу, будут называться logAndAdd impl,
т.е. мы воспользуемся перегрузкой. Одна из этих функций будет принимать универсаль­
ную ссылку. Так что у нас будет одновременно и перегрузка, и универсальные ссылки. Но
каждая функция будет принимать и второй параметр, указывающий, является ли пере­
даваемый аргумент целочисленным значением. Этот второй параметр и будет средством,
избавляющим нас от падений в болото, описанное в разделе 5.4, так как он будет факто­
ром, определяющим выбираемую перегрузку.
Что вы говорите? "Хватит трепа, переходи к делу"? Да хоть сию секунду! Вот почти
корректная версия обновленной функции logAndAdd:
template
void logAndAdd(T&& name )

{
logAndAddimpl ( st d : : forward ( name ) ,
std: : is integral ( ) ) ; / / Не совсем корректно
_

Эта функция передает свой параметр в logAndAddi mpl и при этом передает также ар­
гумент, указывающий, является ли тип параметра (Т) целочисленным. Как минимум
это то, что она должна делать. То же самое она делает и для целочисленных аргумен­
тов, являющихся rvalue. Но, как поясняется в разделе 5.6, если lvаluе-аргумент переда­
ется универсальной ссылке name, то выведенный тип для т будет Ivаluе-ссылкой. Так
что если в функцию l ogAndAdd передается lvalue типа i n t , то тип Т будет выведен как
int &. Но это не целочисленный тип - ссылка таковым не является. Это означает, что
std : : i s_i n t e g ra l будет иметь ложное значение для любого lvаluе-аргумента, даже
если этот аргумент на самом деле является целочисленным значением.
Понимание данной проблемы равносильно ее решению, поскольку в стандартной би­
блиотеке имеется такое средство, как s t d : : remove_refe rence (см. раздел 3.3), которое
делает то, о чем говорит его имя и в чем мы так нуждаемся: удаляет любые квалификато­
ры ссылок из типа. Так что верный способ написания l ogAndAdd имеет следующий вид:
template
void logAndAdd (T&& name )

192

Глава 5 . Rvаluе-ссылки, семанти ка перемещений и п рямая передача

logAndAddimpl (
std : : forward ( name ) ,
std : : i s integral<
typename std: : remove reference: : type
_
>()
);

Это в определенной мере трюк. (Кстати, в С++ 14 можно сэкономить несколько нажатий
клавиш, воспользовавшись вместо выделенного текста st d : : remove r e f e r e nce t .
Подробнее об этом рассказывается в разделе 3.3.)
После принятия этих мер мы можем перенести наше внимание к вызывае­
мой функции, l ogAndAdd i mp l . Имеются две перегрузки, первая из которых приме­
н има к любому нецелочисленному типу (т.е. ко всем типам, для которых значение
std : : i s integra l < t ypename std : : remove_reference : : type> ложно):
11 Нецелочисленный аргумент добавляется
11 в глобальную структуру данных :
template
void logAndAddimpl (T&& name , s t d : : fa lse t ype )
_
{

auto now
std : : chrono : : system_clock : : now ( ) ;
l og ( now, " logAndAdd " ) ;
names . emplace ( std : : forward ( name) ) ;
=

Этот код прост, если вы понимаете механику, лежащую в основе выделенного параметра.
Концептуально logAndAdd передает в функцию logAndAddimpl булево значение, указыва­
ющее, передан ли функции l ogAndAdd целочисленный тип, но значения t rue и f a l s e яв­
ляются значениями времени вьтолнения, а нам для выбора верной версии logAndAddimp l
необходимо разрешение перегрузки, т.е. явление времени компиляции. Это означает, что
нам нужен тип, соответствующий значению t rue, и другой тип, соответствующий зна­
чению f a l s e . Такая необходимость - настолько распространенное явление, что стан­
дартная библиотека предоставляет то, что нам нужно, под именами s t d : : t rue _t уре
и std : : fa l s e_ type. Аргумент, передаваемый в logAndAddimpl функцией logAndAdd,
является объектом типа, унаследованного от std : : t rue_type, если Т
целочисленный
тип, и от std : : fa l s e t ype, если Т таковым не является. Конечный результат заключает­
ся в том, что эта перегрузка logAndAddimpl является реальным кандидатом для вызова
в logAndAdd, только если Т не является целочисленным типом.
Вторая перегрузка охватывает противоположный случай, когда т представляет собой
целочисленный тип. В этом случае l ogAndAddimpl просто ищет имя, соответствующее
целочисленному индексу, и передает это имя функции logAndAdd:
-

_

std : : string nameFromidx ( int idx ) ;

1 1 Как в разделе 5 . 4

1 1 Целочисленный аргумент : поиск имени и
11 вызов с этим именем функции logAndAdd :
5.5. Знакомство с альтернативами переrрузки дnя универсальных ссыпок

1 93

void logAndAddimpl ( int iclx , std: : true_type )
{
logAndAdd ( nameFromidx ( idx ) ) ;

Наличие функции logAndAddimpl для поиска по индексу соответствующего имени
и передача его функции logAndAdd (откуда оно будет передано с помощью std : : forward
друтой перегрузке функции l ogAndAddimpl) позволяет избежать размещения кода для за­
писи в журнале в обеих перегрузках logAndAddimpl.
В таком решении типы std : : t rue_type и std : : false_t ype являются "дескриптора­
ми': единственная цель которых - обеспечить разрешение перегрузки требующимся нам
способом. Обратите внимание, что нам даже не нужны эти параметры. Они не служат
никакой цели во время выполнения, и мы фактически надеемся, что компиляторы рас­
познают, что параметры дескрипторов не используются, и соответствующим образом
оптимизируют выполнимый образ программы. (Некоторые компиляторы так и посту­
пают, по крайней мере иногда.) Вызов перегруженных функций реализации в logAndAdd
"диспетчеризует" передачу работы правильной перегрузке путем создания нужного объ­
екта дескриптора. Отсюда и название этого метода проектирования: диспетчеризация де­
скрипторов. Это стандартный строительный блок шаблонного метапрограммирования,
и чем больше вы будете просматривать код внутри современных библиотек С++, тем
чаще вы будете с ним сталкиваться.
Для наших целей важно не столько то, как работает диспетчеризация дескрипторов,
сколько как она позволяет комбинировать универсальные ссылки и перегрузку без проб­
лем, описанных в разделе 5.4. Функция диспетчеризации - logAndAdd - принимает па­
раметр, являющийся неограниченной универсальной ссылкой, но эта функция не пере­
гружается. Перегружается функция реализации - logAndAddimpl, - которая принимает
параметр, представляющий собой универсальную ссылку, но разрешение вызова этой
функции зависит не только от параметра универсальной ссылки, но и от параметра де­
скриптора, а значения дескрипторов спроектированы таким образом, чтобы было не бо­
лее одной совпадающей перегрузки. В результате то, какая из перегрузок будет вызвана,
определяется дескриптором. Тот факт, что параметр, представляющий собой универсаль­
ную ссылку, всегда генерирует точное соответствие своему аргументу, значения не имеет.

Оr р аничен ия шабл о нов, получающих уни ве рсальные ссылки
Ключевым моментом диспетчеризации дескрипторов является существование (непе­
регруженной) функции в качестве клиентского API. Эта единственная функция распре­
деляет работу между функциями реализации. Обычно создать такую неперегруженную
функцию диспетчеризации несложно, но второй пример, рассмотренный в разделе 5.4,
в котором рассматривался конструктор класса Person с прямой передачей, является ис­
ключением. Компиляторы могут самостоятельно генерировать копирующие и перемеща­
ющие конструкторы, так что, если даже мы напишем один конструктор и используем
в нем диспетчеризацию дескрипторов, некоторые вызовы конструкторов могут быть

1 94

Глава 5. Rvаluе-ссылки, семантика перемещений и прямая передача

обработаны сгенерированными компиляторами функциями, которые обходят систему
диспетчеризации дескрипторов.
По правде говоря, реальная проблема не в том, что генерируемые компиляторами
функции иногда обходят диспетчеризацию дескрипторов; на самом деле она в том, что
они не всегда ее обходят. Вы практически всегда хотите, чтобы копирующий конструктор
класса обрабатывал запрос на копирование lvalue этого типа, но, как показано в разде­
ле 5.4, предоставление конструктора, принимающего универсальную ссылку, приводит
к тому, что при копировании неконстантных lvalue вызывается конструктор с универ­
сальной ссылкой, а не копирующий конструктор. В этом разделе также поясняется, что
когда базовый класс объявляет конструктор с прямой передачей, именно этот конструк­
тор обычно вызывается при традиционной реализации производным классом копиру­
ющего и перемещающего конструкторов, несмотря на то что корректным поведением
является вызов копирующих и перемещающих конструкторов.
Для подобных ситуаций, в которых перегруженная функция, принимающая универ­
сальную ссылку, оказывается более "жадной", чем вы хотели, но недостаточно жадной,
чтобы действовать как единственная функция диспетчеризации, метод диспетчеризации
дескрипторов оказывается не тем, что требуется. Вам нужна другая технология, и эта тех­
нология
std : : еnаЫе i f .
s t d : : е n аЫ е i f дает вам возможность заставить компиляторы вести себя так,
как если бы определенного шаблона не существовало. Такие шаблоны называют от­
ключенными (disaЫed). По умолчанию все шаблоны включены, но шаблон, использу­
ющий s t d : : enaЫe _ i f , включен, только если удовлетворяется условие, определенное
std : : enaЫe_ i f . В нашем случае мы хотели бы включить конструктор Pe rson с прямой
передачей, только если передаваемый тип не является Person. Если переданный тип
Pe rson, то мы хотели бы отключить конструктор с прямой передачей (т.е. заставить ком­
пилятор его игнорировать), поскольку при этом для обработки вызова будет применен
копирующий или перемещающий конструктор, а это именно то, чего мы хотим, когда
один объект типа Pe rson инициализируется другим объектом того же типа.
Способ выражения этой идеи не слишком сложен, но имеет отталкивающий син­
таксис, в особенности если вы не встречались с ним ранее. Имеются некоторые шабло­
ны, располагающиеся вокруг части условия s t d : : еnаЫе _i f, так •по начнем с него. Вот
объявление конструктора с прямой передачей класса P e rson, который показывает не
более чем необходимо для простого использования std : : еnаЫе_ i f. Я покажу только
объявление этого конструктора, поскольку применение std : : еnаЫе_ i f не влияет на ре­
ализацию функции. Реализация остается той же, что и в разделе 5.4:
-

-

class Person {
puЬlic :
template
_

explicit Person ( T & & n ) ;
};

S.S. Знакомство с альтернативами перегрузки для универсальных ссылок

1 95

Вынужден с прискорбием сообщить, что для того, чтобы разобраться, что происхо­
дит в выделенном тексте, вам следует проконсультироваться с друтими источниками ин­
формации, так как в этой книге у меня просто нет места, чтобы подробно все описать.
(В процессе вашего поиска поищите как s t d : : еnаЫе i f, так и волшебную аббревиатуру
"SFINAE", поскольку именно эта технология позволяет работать std : : еnаЫе_ i f.) Здесь
я хочу сосредоточиться на выражении условия, которое управляет тем, является ли кон­
структор включенным.
Условие, которое мы хотим указать, - что тип Т не является Person, т.е. что шаблони­
зированный конструктор может быть включенным, только если т является типом, отлич­
ным от Person. Благодаря свойствам шаблонов мы можем определить, являются ли два
типа одним и тем же ( s t d : : is _ same ) , так что создается впечатление, что интересующее
нас условие можно записать как ! std : : i s same < Person , Т> : : value. (Обратите внимание
на символ " ! " в начале выражения. Мы хотим, чтобы типы Person и Т не совпадали.)
Это близко к тому, что нам надо, но не совсем верно, поскольку, как поясняет раздел 5.6,
тип, выведенный для универсальной ссылки, инициализированной lvalue, всегда являет­
ся lvаluе-ссылкой. Это означает, что в коде наподобие
_

Person p ( "Nancy" ) ;
auto cloneOfP ( p ) ;

11 Инициализация

с

помощью lvalue

тип Т в универсальном конструкторе будет выведен как P e r s o n & . Типы P e r s o n
и P e r s o n & - разные, и результат s t d : : i s _same отражает этот факт: значение
std : : is _ same < Person , Person& > : : value ложно.
Если разобраться, что означает, что шаблонный конструктор в классе Person должен
быть включен только тогда, когда Т не является Pers on, то мы поймем, что, глядя на Т,
мы хотим игнорировать следующее.


Ссылки. С точки зрения определения, должен ли быть включен конструктор с уни­
версальной ссылкой, типы Person, Person& и Perso n & & должны рассматриваться
как идентичные типу Person.



Модификаторы const и volatile. С той же точки зрения типы cons t Person,
vola t i l e Person и const vo lat i l e Person должны рассматриваться как иден­
тичные типу Person.

Это означает, что нам нужен способ удалить все ссылки, const и volat i l e из типа Т
перед тем как выяснять, совпадает ли он с типом Person. И вновь на выручку приходит
стандартная библиотека, предоставляя шаблон std : : decay. Тип std : : decay : : t ype
представляет собой то же самое, что и тип Т, но из него удалены все ссылки и квалифи­
каторы const и volat i le. (Я немного вас обманул, потому что std : : decay, кроме того,
превращает массивы и типы функций в указатели (см. раздел 1 . 1 ) , но для наших целей
можно считать, что std : : decay ведет себя так, как я описал.) Условие, которое должно
выполняться для включения рассматриваемого конструктора, имеет вид
' std : : is_same : : value

1 96

Гnава 5. Rvalue-ccыnки, семантика перемещений и прямая передача

т.е. Person не совпадает с типом Т, без учета всех ссылок и квалификаторов const
и volat i l e. (Как поясняется в разделе 3.3, ключевое слово t ypename перед std: : decay
необходимо, поскольку тип s t d : : decay : : t ype зависит от параметра шаблона Т. )
Вставка этого условия в шаблон std : : enaЫe_if выше, а также форматирование ре­
зультата для того, чтобы проще понять взаимоотношения между частями кода, дает сле­
дующее объявление конструктора с прямой передачей класса Person:
class Person {
puЫic :
template<
typename Т,
typename = typename s t d : : enaЫe if<
! std : : is same: : value

> : : type
>
explicit Person (T&& n ) ;
};

Если вы никогда ранее не видели ничего подобного, не пугайтесь. Есть причина,
по которой я оставил этот метод напоследок. Если для того, чтобы избежать смешивания
универсальных ссылок и перегрузки вы можете использовать один из прочих методов (а
это почти всегда возможно), вы должны это сделать. Тем не менее, если вы привыкнете
к функциональному синтаксису и множеству угловых скобок, это не так плохо. Кроме
того, это позволяет получить поведение, к которому вы стремитесь. С учетом приведен­
ного выше объявления построение объекта Person из другого объекта Person (lvalue или
rvalue, с квалификатором const или без него, с квалификатором volat i l e или без него)
никогда не вызовет конструктор, принимающий универсальную ссылку.
Мы добились успеха? Дело сделано?
Пока что нет. Не спешите праздновать. Раздел 5.4 все еще посылает нам свои приветы.
Нам надо заткнуть этот фонтан окончательно.
Предположим, что класс, производный от Pe rson, реализует операции копирования
и перемещения традиционным способом:
class SpecialPerson : puЬlic Person {
puЫic :
SpecialPerson ( const Specia l Pe rson& rhs ) / / Копирующий
// конструктор; вызывает конструктор
Person (rhs)
/ / базового класса с прямой передачей !
}
[
".

SpecialPe rson ( Special Person& & rhs )
// Перемещающий
Person (std: : move (rhs) ) / / конструктор; вызывает конструктор
/ / базового класса с прямой передачей !
( ... }
};

S.S. Знакомство с аnьтернативами переrрузки дnя универсаnьных ссыпок

1 97

Это тот же код, который вы видели ранее, в конце предыдущего раздела, вклю­
чая комментарии, увы, оставшиеся справедливыми. Копируя или перемещая объект
SpecialPerson, мы ожидаем, что части базового класса будут скопированы или переме­
щены с помощью копирующего или, соответственно, перемещающего конструктора ба­
зового класса. Однако в этих функциях мы передаем объекты Specia lPerson конструк­
торам базового класса, а поскольку Spe c i a l Person не совпадает с Person (даже после
применения s t d : : decay}, конструктор с универсальной ссылкой в базовом классе ока­
зывается включенным и без проблем проходит проверку на идеальное совпадение с ар­
гументом Speci a l Person. Это точное соответствие лучше преобразования производного
класса в базовый, необходимого для связывания объекта Spe c i a l Person с параметром
Person в копирующем и перемещающем конструкторах класса Person, так что при име­
ющемся коде копирование и перемещение объектов Special Person будет использовать
для копирования и перемещения частей базового класса конструктор с универсальной
ссылкой класса Person! Это чудное ощущение дежавю раздела 5.4 . . .
Производный класс просто следует обычным правилам реализации копирующего
и перемещающего конструкторов производного класса, поэтому решение этой проблемы
находится в базовом классе и, в частности, в условии, которое контролирует включение
конструктора с универсальной ссылкой класса Person. Теперь мы понимаем, что надо
включать шаблонный конструктор не для любого типа аргумента, отличного от Person,
а для любого типа аргумента, отличного как от Person, так и от типа, производного
от P e r s on. Ох уж это наследование!
Вас уже не должно удивлять обилие всяческих полезных шаблонов в стандартной
библиотеке, так что известие о наличии шаблона, который определяет, является ли
один класс производным от другого, вы должны воспринять с полным спокойстви­
ем. Он называется std : : i s_base of. Значение std : : i s_base o f : : va lue ис­
тинно, если Т2
класс, производный от T l . Пользовательские типы рассматриваются
как производные от самих себя, так что std : : is _base_o f : : value истинно, если
Т представляет собой пользовательский тип. (Если Т является встроенным типом,
s t d : : is_base_of : : va lue ложно.) Это удобно, поскольку мы хотим пересмотреть
наше условие, управляющее отключением конструктора с универсальной ссылкой класса
Person таким образом, чтобы этот конструктор был включен, только если тип Т после
удаления всех ссылок и квалификаторов const и volat i l e не являлся ни типом Person,
ни классом, производным от Person. Применение std : : is _base_of вместо std : : is same
дает нам то, что требуется:
-

_

class Person {
puЬli c :
template<
typename Т,
typename = typename std : : enaЫe if<
! std: : is_Ьase_of : : value

198

Гnава S . Rvalue-ccыnки, семантика перемещений и прямая передача

> : : type
>
explicit Person (T&& n ) ;
)

;

Вот теперь работа завершена. Вернее, завершена при условии, что мы пишем код
на С++ l l . При использовании С++ 1 4 этот код будет работать, но мы можем использо­
вать псевдонимы шаблонов для std : : еnаЫе if и s t d : : decay, чтобы избавиться от хла­
ма в виде t ypename и : : t уре, получая несколько более приятный код:
class Person {
1 1 С++ 1 4
puЫ i c :
template<
typename Т,
typename
std : : enaЬle if t<
1 1 Меньше кода здесь
! std: : is_base_o f : : value
>
11 И здесь
>
=

explicit Person (T&& n ) ;
)

;

Ладно, я признаю: я соврал. Мы до сих пор не сделали всю работу. Но мы уже близки
к завершению. Соблазнительно близки. Честно!
Мы видели, как использовать std : : е nаЫе_ i f для выборочного отключения конструк­
тора с универсальной ссылкой класса Person для типов аргументов, которые мы хотим
обрабатывать с помощью копирующего и перемещающего конструкторов, но мы еще не
видели, как применить его для того, чтобы отличать целочисленные аргументы от не яв­
ляющихся таковыми. В конце концов, таковой была наша первоначальная цель; проблема
неоднозначности конструктора была всего лишь неприятностью, подхваченной по дороге.
Все, что нам нужно сделать (и на этот раз "все" действительно означает, что это уже
все), - это ( l ) добавить перегрузку конструктора Person для обработки целочисленных
аргументов и (2) сильнее ограничить шаблонный конструктор так, чтобы он был отклю­
чен для таких аргументов. Положите эти ингредиенты в кастрюлю со всем остальным,
что мы уже обсуждали, варите на медленном огне и наслаждайтесь ароматом успеха:
class Person {
puЫ i c :
template<
typename Т,
typename
std : : enaЬle i f t<
! std : : i s_ba se_of : : value
=

5.5. Знакомство с альтернативами перегрузки для универсальных ссып ок

199

&&
! std: : is integral : : value
_
_
_

>
>
/ / Конструктор для std : : striпg и
explicit Persoп ( T& & п )
пame ( s td : : forward ( п ) ) / / аргументов, приводимь� к
/ / std : : striпg
{
}
.. .

explicit Person (int idx)
name (nameFromidx (idx) )
1

...

11 Конструктор для
/ / целочисленных аргументов

}

11 Копирующий и перемещающий конструкторы и т . д .
private :
std : : striпg паmе ;

};
Красота! Ну ладно, красота в основном для тех, кто фетишизирует метапрограмми­
рование с использованием шаблонов, но факт остается фактом: этот подход не только
справляется с работой, но и делает это с чрезвычайным апломбом. Применение прямой
передачи предполагает высокую эффективность, а управление сочетанием универсаль­
ных ссылок и перегрузки вместо их запрета позволяет применять этот метод в обстоя­
тельствах (таких, как разработка конструкторов), когда перегрузка неизбежна.

Ко м п ромиссы
Первые три рассмотренные в данном разделе метода - отказ от перегрузки, передача
coпst Т& и передача по значению - указывают тип каждого параметра в вызываемой
функции или функциях. Последние два метода - диспетчеризация дескрипторов и огра­
ничения шаблонов - используют прямую передачу, а следовательно, типы параметров
не указывают. Это фундаментальное решение - указывать типы или нет - имеет свои
следствия.
Как правило, прямая передача более эффективна, потому что позволяет избежать соз­
дания временных объектов исключительно с целью соответствия типу объявления па­
раметра. В случае конструктора Person прямая передача допускает передачу строкового
литерала, такого как "Nancy", непосредственно в конструктор для std : : st riпg внутри
Persoп, в то время как методы, не использующие прямой передачи, вынуждены созда­
вать временный объект std : : striпg из строкового литерала для удовлетворения спец­
ификации параметра конструктора Persoп.
Но прямая передача имеет свои недостатки. Один из них тот, что некоторые виды
аргументов не могут быть переданными прямой передачей, несмотря на то что они могут
быть переданы функциям, принимающим конкретные типы. Эти сбои прямой передачи
исследуются в разделе 5.8.
Второй неприятностью является запутанность сообщений об ошибках, когда клиен­
ты передают недопустимые аргументы. Предположим, например, что клиент, создающий
200

Гл ава 5. Rvalue-ccыnки, семантика перемещений и прямая передача

объект Pers on, передает строковый литерал, составленный из символов cha r l б_t (тип
С++ 1 1 для представления 1 6-разрядных символов) вместо char (из которых состоит
std : : st ring ) :
Person p ( u"Konrad Zuse" ) ; // "Konrad Zuse" состоит иэ
// символов типа const charlб t

При использовании первых трех из рассмотренных в данном разделе подходов
компиляторы увидят, что доступные конструкторы могут принимать либо i n t , либо
std : : s t r ing, и выведут более или менее понятное сообщение об ошибке, поясняющее,
что не существует преобразования из const charl б_t [ 12 ) в int или s t d : : s t r i ng.
Однако при подходе с использованием прямой передачи массив const char l б t
связывается с параметром конструктора без замечаний и жалоб. Оттуда он передается
конструктору члена-данных типа std : : str ing класса Person, и только в этот момент об­
наруживается несоответствие между тем, что было передано (массив const cha rl 6_t ) ,
и тем, что требовалось (любой тип, приемлемый для конструктора std : : st ring ) . В ре­
зультате получается впечатляющее сообщение об ошибке. Так, в одном из компиляторов
оно состояло более чем из 1 60 строк!
В этом примере универсальная ссылка передается только один раз (из конструктора
Person в конструктор s t d : : s t r ing ), но чем более сложна система, тем больше вероят­
ность того, что универсальная ссылка передается через несколько слоев вызовов функций
до того, как достигнет точки, в которой определяется приемлемость типа аргумента (или
типов). Чем большее количество раз будет передаваться универсальная ссылка, тем более
непонятными и громоздкими будут выглядеть сообщения об ошибках, если что-то пойдет
не так. Многие разработчики считают, что одно это является основанием для применения
универсальных ссылок только там, где особенно важна производительность.
В случае класса Person мы знаем, что параметр универсальной ссылки передающей
функции должен выступать в роли инициализатора для std : : st r i ng, так что мы можем
использовать stat ic assert для того, чтобы убедиться в его пригодности для этой роли.
Свойство типа std : : i s_cons t ruct iЫe выполняет проверку времени компиляции того,
может ли объект одного типа быть построен из объекта (или множества объектов) дру­
гого типа (или множества типов), так что написать такую проверку несложно:
_

class Person {
puЫ ic :
11 Как и ранее
template<
typename Т,
typename = std : : enaЫe if t <
1 std : : is_base_of : : value
&&
! std: : i s_integra l : : value
>
>
expl icit Person ( T&& n )
: name ( std: : forward ( n ) )

5.5. Знакомство с аnьтернативами перегрузки дnи универсаnьных ссыnок

201

1 1 Проверка возможности создания std: : string из Т
static_assert (
std: : is constructiЫe : : value ,
_
"П8рам8'1'р n

не

может использоваться Д11Я "

"конаrруиро:вания а std: : string"
);

11 Здесь идет код обычного конструктора
1 1 Остальная часть класса Person ( как ранее)
};

В результате при попытке клиентского кода создать объект Person из типа, непри­
годного для построения std : : s t ri ng, будет выводиться указанное сообщение об ошиб­
ке. К сожалению, в этом примере s t a t ic _a s s e rt находится в теле конструктора, а пе­
редающий код, являясь частью списка инициализации членов, ему предшествует. Ис­
пользовавшиеся мною компиляторы выводят ясное и понятное сообщение об ошибке
от s t a t i c_a s sert, но только после обычных сообщений (всех этих 160 с лишним строк).
Следует запомнит ь


Альтернативы комбинации универсальных ссылок и перегрузки включают
использование различных имен, передачу параметров как lvalue-ccылoк
на c o n s t, передачу параметров по значению и использование диспетчеризации
дескрипторов.



Ограничение шаблонов с помощью s t d : : e n a Ы e _ i f позволяет использовать
универсальные ссылки и перегрузки совместно, но управляет условиями, при
которых компиляторы могут использовать перегрузки с универсальными
ссылками.



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

S .6. Свертывание ссыл ок
В разделе 5. 1 говорилось, что, когда аргумент передается в шаблонную функцию, вы­
веденный для параметра шаблона тип указывает, является ли аргумент lvalue или rvalue.
В разделе не было упомянуто, что это происходит только тогда, когда аргумент использу­
ется для инициализации параметра, являющегося универсальной ссылкой, но тому есть
уважительная причина: универсальные ссылки появились только в разделе 5.2. Вместе
эти наблюдения об универсальных ссылках и кодировании lvalue/rvalue означают, что
для шаблона
template
void func ( T & & param) ;
202

Глава 5. Rvаluе-ссылки, семантика перемещений и прямая передача

выведенный параметр шаблона Т будет включать информацию о том, был ли переданный
в param аргумент lvalue или rvalue.
Механизм этого кодирования прост. Если в качестве аргумента передается lvalue, т
выводится как \vа\uе-ссылка. При передаче rvalue вывод типа приводит к тому, что т не
является ссылкой. (Обратите внимание: lvalue кодируются как \vаluе-ссылки, но rvalue
кодируются как не ссылки.) Следовательно:
Widget widgetFactory ( ) ; 1 1
Widget w ;
11
func (w) ;
11
11
func (widgetFactory ( ) ) ; 1 1
11

Функция, возвращающая rvalue
Переменная ( lva lue )
Вызов функции с lvalue ; тип т
представляет собой Widget&
Вызов функции с rvalue ; тип т
представляет собой Widget

В обоих вызовах func передается W i dget, но так как один W i dget является lvalue,
а второй представляет собой rvalue, для параметра шаблона Т выводятся разные
типы. Это, как вы вскоре увидите, и определяет, чем становятся универсальные ссыл ки: rvalue- или lvа\uе-ссылками; а кроме того, это механизм, лежащий в основе работы
s t d : : forward.

Прежде чем более внимательно рассмотреть std : : forward и универсальные ссылки,
мы должны заметить, что ссылка на ссылку в С++ не существует. Попытайтесь объявить
ее, и компилятор вынесет вам строгий выговор:
int

х;

auto& & r x

=

х;

/ / Ошибка ! Объявлять ссылки н а ссьщки нельзя

Но рассмотрим, что произойдет, если передать lvalue шаблону функции, принимаю­
щему универсальную ссылку:
template
void func ( T & & param) ; / / Как и ранее
func (w) ;
1 1 Вызов func с lvalue ;
/ / Т выводится как Widget&

Если мы возьмем тип, выведенный для Т (т.е. Widge t & ) и используем его для инстан­
цирования шаблона, то получим следующее:
void func (Widget& && param) ;

Ссылка на ссылку! Но компиляторы не возражают. Из раздела 5.2 мы знаем, что, посколь­
ку универсальная ссылка pa ram инициализируется с помощью lvalue, тип pa ram должен быть
\vаluе-ссылкой, но как компилятор получит результат взятия выведенного типа для т и под­
становки его в шаблон, который представляет собой конечную сигнатуру функции?
void func (Widget& param) ;

Ответ заключается в свертывании ссылок (reference collapsing). Да, вам запреще­
но объявлять ссылки на ссылки, но компиляторы могут создавать их в определенных

S .б. С вертывание ссы л ок

203

контекстах, среди которых - инстанцирование шаблонов. Когда компиляторы генериру­
ют ссылки на ссылки, свертывание ссылок определяет, что будет дальше.
Существуют два вида ссылок (lvalue и rvalue), так что имеются четыре возможные
комбинации "ссылка на ссылку" (lvalue на lvalue, lvalue на rvalue, rvalue на lvalue и rvalue
на гvalue). Если ссылка на ссылку возникает в контексте, где это разрешено (например,
во время инстанцирования шаблона}, то ссылки сворачиваются в единственную ссылку
согласно следующему правилу:
Если любая из ссылок является lvаluе-ссылкой, результат представляет собой
lvalue-ccылкy. В противном случае (т.е. когда обе ссылки являются rvаluе-ссылками)
результат представляет собой гvаluе-ссылку.
В нашем приведенном выше примере подстановка выведенного типа Widget& в ша­
блон func дает rvalue-ccылкy на lvalue-ccылкy, и правило свертки ссылок гласит, что ре­
зультатом является lvalue-ccылкa.
Свертывание ссылок является ключевой частью механизма, обеспечивающего работу
std : : forward. Как пояснялось в разделе 5.3, s t d : : forward применяется к параметрам,
являющимся универсальными ссылками, так что обычно его применение имеет следую­
щий вид:
template
void f ( T&& fParam)
11 Некоторая работа
someFunc (std : : forward ( f Param) ) ; / / Передача fParam в
1 1 someFunc

Поскольку f Pa ram представляет собой универсальную ссылку, мы знаем, что пара­
метр типа т будет кодировать информацию о том, являлся ли переданный f аргумент
(т.е. выражение, использованное для инициализации f Pa ram} lvalue или rvalue. Работа
s t d : : forward заключается в приведении fPa ram (lvalue) к rvalue тогда и только тогда,
когда Т гласит, что переданный в f аргумент был rvalue, т.е. если Т не является ссылоч­
ным типом.
Вот как можно реализовать std : : forward, чтобы он выполнял описанные действия:
t emplate
1 1 В пространстве имен std
Т&& forward ( t ypename
remove_reference : : type& param)
return stat ic_cast (param) ;

Этот код не совсем отвечает стандарту (я опустил несколько деталей интерфейса), но
отличия не играют роли для понимания того, как ведет себя std : : forward.

204

Гnава 5. Rvalue-ccыnки, семантика перемещений и прямая передача

Предположим, что аргумент, переданный f, является lvalue типа Widget . Тип Т будет
выведен как Widget &, а вызов s t d : : forward инстанцирует std : : forward. Под­
становка Widget & в реализацию s t d : : forward дает следующее:
Wiclget& && forward ( typename
remove_reference : : type& param)
{ return static_cast ( param) ; )

Свойство типа std : : remove_ reference : : t ype дает Widget (см. раздел 3.3),
так что std : : forward превращается в
Widget & && forward (Wiclget& param)
{ return static_cas t ( param) ;

К возвращаемому типу и приведению также применяется сворачивание ссылок, и ре­
зультат представляет собой последнюю версию std : : forward для вызова:
Wiclget& forward (Widget& param)
{ return static_cast ( param) ;

1 1 В пространстве
11 имен std

Как можно видеть, когда в шаблон функции f передается аргумент lvalue,
s t d : : forward инстанцируется для получения и возврата \vа\uе-ссылки. Приведение

внутри st d : : forward не делает ничего, поскольку тип param уже представляет собой
Widget &, так что приведение его к Widge t & ни на что не влияет. Таким образом, \vа\uе­
арrумент, переданный std : : forward, вернет \vа\uе-ссылку. По определению \vаluе-ссылки
являются lvalue, так что передача lvalue в std : : forward приводит к возврату lvalue, как
и предполагалось.
Предположим теперь, что передаваемый f аргумент является rvalue типа W i dg e t .
В этом случае выведенный тип параметра типа Т шаблона f будет просто Widget. Вызов
std : : forward внутри f, таким образом, будет представлять собой std : : forward.
Подстановка Widget вместо Т в реализации s t d : : forward дает следующее:
Wiclget& & forward ( t ypename
remove_reference : : type& param)
{ return static_cast ( param) ; )

Применение s t d : : remove_re ference к типу W idget, не являющемуся ссылкой, дает
тот же тип, что и переданный (W idget ) , так что std : : forward превращается в
Widget & & forward (Wiclget& param)
{ return static_cast (param) ;

Здесь нет ссылок на ссылки, так что нет и свертывания ссылок, и это последняя ин­
станцированная версия s t d : : forward для этого вызова.
Так как rvа\uе-ссылки, возвращаемые из функции, определены как rvalue, в этом слу­
чае std : : forward превратит параметр f Param (lvalue) функции f в rvalue. Конечным ре­
зультатом является то, что rvаluе-аргумент, переданный функции f, будет передан функ­
ции someFunc как rvalue, и это именно то, что и должно было произойти.

S.6. С вертывание ссылок

205

Наличие в С++ 1 4 s t d : : r emove r e f e r e n c e t делает возможным реализовать
std : : forward немного более лаконично:
/ / С++ 1 4 ; в
template
Т&& forward ( reшove reference_t& param) / / пространстве
11 имен std
return static_cast (param) ;

Свертывание ссылок происходит в четырех контекстах. Первый и наиболее распро­
страненный - инстанцирование шаблонов. Второй - генерация типов для перемен­
ных auto. Детали, по сути, те же, что и для шаблонов, поскольку вывод типа для аutо­
переменных, по сути, совпадает с выводом типов для шаблонов (см. раздел 1 .2). Рассмо­
трим еще раз пример, приводившийся ранее в данном разделе:
template
void func ( T & & pararn) ;
Widget widgetFactory ( ) ; 1 1
Widget w;
11
11
func ( w ) ;
11
func ( widgetFactory ( ) ) ; 1 1
11

Функция , возвращающая rvalue
Переменная ( lvalue )
Вызов функции с lvalue ; тип т
представляет собой Widget&
Вызов функции с rvalue; тип т
представляет собой Widget

Это можно имитировать в виде auto. Объявление
auto&& wl

w;

=

инициализирует w l с помощью lvalue, выводя, таким образом, для auto тип Widget & .
Подстановка W idget & вместо auto в объявление для w l дает код со ссылкой на ссылку
Widget& && wl

=

w;

который после сворачивания ссылок принимает вид
Widget& wl

=

w;

В результате w l представляет собой lvalue-ccылкy.
С другой стороны, объявление
auto&& w2

=

widgetFactory ( ) ;

инициализирует w2 с помощью rvalue, приводя к тому, что для aut o выводится тип
Widget, не являющийся ссылкой. Подстановка Widget вместо auto дает
Widget&& w2

=

widgetFactory ( ) ;

Здесь нет ссылок на ссылки, так что процесс завершен; w2 представляет собой rvalue­
ccылкy.

206

Гnа ва S . Rvalue-ccыnки, семантика перемещений и прямая передача

Теперь мы в состоянии по-настоящему понять универсальные ссылки, введенные
в разделе 5.2. Универсальная ссылка не является новой разновидностью ссылок, в дей­
ствительности это rvalue-ccылкa в контексте, в котором выполняются два условия.


Вывод типа отличает lvalue от rvalue.



Происходит свертывание ссылок.

lvalue типа т выводится как имеющее тип
в то время как rvalue типа т дает в качестве выведенного типа Т .

Т&,

Концепция универсальных ссылок полезна тем, что избавляет вас от необходимости
распознавать наличие контекстов сворачивания, мысленного вывода различных типов
для lvalue и rvalue и применения правила свертывания ссылок после мысленной подста­
новки выведенных типов в контексты, в которых они встречаются.
Я говорил, что имеется четыре контекста, но мы рассмотрели только два из них: ин­
станцирование шаблонов и генерацию типов auto. Третьим является генерация и ис­
пользование typede f и объявлений псевдонимов (см. раздел 3.3). Если во время создания
или вычисления t ypede f возникают ссылки на ссылки, для их устранения применяет­
ся сворачивание ссылок. Предположим, например, что у нас есть шаблон класса Widget
с внедренным t ypedef для типа rvаluе-ссылки
template
class Widget {
puЫic :
typedef Т&& RvalueRefToT;
};

и предположим, что мы инстанцируем W idget с помощью типа lvаluе-ссылки:
Widget w;

Подстановка i nt & вместо Т в шаблоне W i dge t дает нам следующую конструкцию
typedef:
typede f int& && RvalueRefToT;

Сворачивание ссылок приводит этот код к
typedef int& RvalueRefToT;

Теперь ясно, что имя, которое мы выбрали для t ypede f, вероятно, не настолько описа­
тельно, как мы надеялись: Rva l u e Re fToT представляет собой typedef для lvalue-ccьmкu,
когда Widget инстанцируется типом lvаluе-ссылки.
Последним контекстом, в котором имеет место сворачивание ссылок, является ис­
пользование dec l t уре. Если во время анализа типа, включающего decl t уре, возникает
ссылка на ссылку, она устраняется сворачиванием ссылок. (Информацию о decltype вы
найдете в разделе 1 .3.)

S.б. С вертывание ссыпок

207

Следует запомнить


Сворачивание ссылок встречается в четырех контекстах: инстанцирование шаблона,
генерация типа auto, создание и применение t ypede f и объявлений псевдонимов,
и decl t ype.



Когда компиляторы генерируют ссылку на ссылку в контексте сворачивания ссылок,
результатом становится единственная ссылка. Если любая из исходных ссылок явля­
ется lvаluе-ссылкой, результатом будет lvalue-ccылкa; в противном случае это будет
rvalue-ccылкa.



Универсальные ссылки представляют собой rvа\uе-ссылки в контекстах, в которых
вывод типов отличает lvalue от rvalue и происходит сворачивание ссылок.

S.7. Счита йте, что перемещающие операции
отсутствуют, дороrи иnи не испоnьзуются
Семантика перемещения, пожалуй, самая главная возможность С++ 1 1 . Вам наверня­
ка приходилось слышать, что "перемещение контейнеров теперь такое же дешевое, как
и копирование указателей" или что "копирование временных объектов теперь настолько
эффективно, что избегать его равносильно преждевременной оптимизации': Понять та­
кие настроения легко. Семантика перемещения действительно является очень важной
возможностью. Она не просто позволяет компиляторам заменять дорогостоящие опера­
ции копирования относительно дешевыми перемещениями, но и требует от них этого
(при выполнении надлежащих условий). Возьмите ваш код С++98, перекомпилируйте его
с помощью компилятора и стандартной библиотеки С++ 1 1 и - о чудо! - ваша програм­
ма заработает быстрее.
Семантика перемещения действительно в состоянии осуществить все это - и потому
достойна легенды. Однако легенды, как правило, - это результат преувеличения. Цель
данного раздела - спустить вас с небес на землю.
Начнем с наблюдения, что многие типы не поддерживают семантику перемещения.
Вся стандартная библиотека С++98 была переработана с целью добавления операций
перемещения для типов, в которых перемещение могло быть реализовано быстрее копи­
рования, и реализации компонентов библиотеки были пересмотрены с целью использо­
вания преимуществ новых операций; однако есть вероятность, что вы работаете с кодом,
который не был полностью переделан под C++ l l . Для типов в ваших приложениях (или
в используемых вами библиотеках), в которые не были внесены изменения для С++ 1 1 ,
мало пользы от наличия поддержки перемещения компилятором. Да, С++ 1 1 готов ге­
нерировать перемещающие операции для классов, в которых они отсутствуют, но это
происходит только для классов, в которых не объявлены копирующие операции, пере­
мещающие операции или деструкторы (см. раздел 3. 1 1 ). Члены-данные базовых классов
типов, в которых перемещения отключены (например, путем удаления перемещающих
операций; см. раздел 3.5) также подавляют перемещающие операции, генерируемые
208

Глава 5. Rvаluе-ссылки, семантика перемещений и прямая передача

компиляторами. Для типов без явной поддержки перемещения и типов, которые не могут
претендовать на перемещающие операции, генерируемые компилятором, нет оснований
ожидать что С++ 1 1 обеспечит повышение производительности по сравнению с С++98.
Даже типы с явной поддержкой перемещений не могут обеспечить все, на что вы на­
деетесь. Например, все контейнеры стандартной библиотеки С++ 1 1 поддерживают пере­
мещение, но было бы ошибкой считать, что перемещение является дешевой операцией
для всех контейнеров. Для одних контейнеров это связано с тем, что нет никакого дей­
ствительно дешевого способа перемещения их содержимого. Для других - с тем, что
действительно дешевые перемещающие операции предлагаются контейнерами с оговор­
ками, которым не удовлетворяют конкретные элементы контейнера.
Рассмотрим новый контейнер C++ l l - s t d : : a r r a y . Контейнер s t d : : a r r a y,
по сути, представляет собой встроенный массив с SТL-интерфейсом. Он фундамен­
тально отличается от других стандартных контейнеров, которые хранят свое содержи­
мое в динамической памяти. Объекты таких типов контейнеров концептуально содер­
жат (в качестве членов-данных) только указатель на динамическую память, хранящую
содержимое контейнера. (Действительность более сложна, но для наших целей эти от­
личия не играют роли.) Наличие такого указателя позволяет перемещать содержимое
всего контейнера на константное время: просто копируя указатель на содержимое кон­
тейнера из исходного контейнера в целевой и делая указатель исходного контейнера
нулевым:
std: : vector vw l ;
// Размещение данных в vwl

vwl

Widgets

vwl

/ / Перемещение vwl в vw2 . ВЬПlолняется
11 за константное время, изменяя
// только указатели в vwl и vw2
auto vw2
std : : move (vwl ) ;

Widgets

lnиllJ

=

Объекты std : : array не содержат такого указателя, поскольку данные, содержащиеся
в std : : array, хранятся непосредственно в объекте std : : a rray:
std: : array awl ;
11 Размещение данных в vwl

1

awl

'

Widgets

��Ф 4J4ФZ!&!QZLCJtЮИ Ц щ$

1



awl

// Перемещение vwl в vw2 . ВЬПlолняется
// за линейное время . Все элементы
11 awl перемещаются в aw2
auto aw2
std: : move (awl ) ;
=

Widgets (перемещение отсюда)

·

aw2

Widgets ((перемещение сюда)

Обратите внимание, что все элементы из awl перемещаются в aw2. В предположе­
нии, что W idget представляет собой тип, операция перемещения которого выполняется
быстрее операции копирования, перемещение std : : array элементов Widget будет более
5.7.

Считайте, что перемеща ющие операции отсутствуют, дороrи ипи не используются

209

быстрым, чем копирование того же std : : array. Поэтому std : : a rray предлагает под­
держку перемещения. И копирование, и перемещение std : : a rray имеют линейное время
работы, поскольку должен быть скопирован или перемещен каждый элемент контейнера.
Это весьма далеко от утверждения "перемещение контейнеров теперь такое же дешевое,
как и копирование указателей", которое иногда приходится слышать.
С другой стороны, std : : s t r i ng предлагает перемещение за константное время и ко­
пирование - за линейное. Создается впечатление, что в этом случае перемещение бы­
стрее копирования, но это может и не быть так. Многие реализации строк используют
оптимизацию малых строк (small string optimization - SSO). При использовании SSO
"малые" строки (например, размером не более 1 5 символов) хранятся в буфере в самом
объекте s t d : : s t r i ng; выделение динамической памяти не используется. Перемещение
малых строк при использовании реализации на основе SSO не быстрее копирования, по­
скольку трюк с копированием только указателя наданные, который в общем случае обе­
спечивает повышение эффективности, в данном случае не применим.
Мотивацией применения SSO является статистика, указывающая, что короткие стро­
ки являются нормой для многих приложений. С помощью внутреннего буфера для хра­
нения содержимого таких строк устраняется необходимость динамического выделения
памяти для них, и это, как правило, дает выигрыш в эффективности. Следствием этого
выигрыша является то, что перемещение оказывается не быстрее копирования" . Хотя
для любителей наполовину полного стакана·' можно сказать, что для таких строк копиро­
вание не медленнее, чем перемещение.
Даже для типов, поддерживающих быстрые операции перемещения, некоторые ка­
жущиеся очевидными ситуации могут завершиться созданием копий. В разделе 3.8 по­
ясняется, что некоторые контейнерные операции в стандартной библиотеке предпола­
гают строгие гарантии безопасности исключений и что для гарантии того, что старый
код С++98, зависящий от этой гарантии, не станет неработоспособным при переходе
на С++ 1 1 , операции копирования могут быть заменены операциями перемещения, толь­
ко если известно, что последние не генерируют исключений. В результате, даже если тип
предоставляет перемещающие операции, более эффективные по сравнению с соответ­
ствующими копирующими операциями, и даже если в определенной точке кода пере­
мещающая операция целесообразна (например, исходный объект представляет собой
rvalue), компиляторы могут быть вынуждены по-прежнему вызывать копирующие опе­
рации, поскольку соответствующая перемещающая операция не объявлена как noexcept.
Таким образом, имеется ряд сценариев, в которых семантика перемещения С++ 1 1 не­
пригодна.


Отсутствие перемещающих операций. Объект, из которого выполняется переме­
щение, не предоставляет перемещающих операций. Запрос на перемещение, таким
образом, превращается в запрос на копирование.



Перемещение не быстрее. Объект, из которого выполняется перемещение, имеет
перемещающие операции, которые не быстрее копирующих.

' Известная история о том, что об одном и том же до сре11ины напол11е1111ом стакане пессимисты
говорят, что он наполовину пуст, а оптимисты - что он наполовину полон. - Примеч. пер.
210

Глава S. Rvаluе-ссы л ки, семантика перемещений и прямая передача



Контекст, в котором должно иметь место перемеще­
ние, требует операцию, не генерирующую исключения, но операция перемещения
не объявлена как noexcept.

Перемещение неприменимо.

Стоит упомянуть также еще один сценарий, когда семантика перемещения не приво­
дит к повышению эффективности.


Исходный объект является lvalue. За очень малыми исключениями (см., например,
раздел 5.3) только rvalue могут использоваться в качестве источника перемещающей
операции.

Но название этого раздела предполагает отсутствие перемещающих операций, их
дороговизну или невозможность использования. Это типично для обобщенного кода,
например, при написании шаблонов, поскольку вы не знаете всех типов, с которыми
придется работать. В таких условиях вы должны быть настолько консервативными в от­
ношении копирования объектов, как будто вы работаете с С++98, - до появления се­
мантики перемещения. Это также случай "нестабильного" кода, т.е. кода, в котором ха­
рактеристики используемых типов относительно часто изменяются.
Однако зачастую вам известно, какие типы использует ваш код, и вы можете по­
ложиться на неизменность их характеристик (например, на поддержку ими недорогих
перемещающих операций). В этом случае вам не надо делать такие грустные предполо­
жения. Вы просто изучаете детали поддержки операций перемещения, используемых ва­
шими типами. Если эти типы предоставляют недорогие операции перемещения и если
вы используете объекты в контекстах, в которых эти операции будут вызываться, можете
безопасно положиться на семантику перемещений при замене копирующих операций их
менее дорогими перемещающими аналогами.
Сnедует запомнить


Считайте, что перемещающие операции отсутствуют, дороги или не используются.



В коде с известными типами или поддержкой семантики перемещения нет необходи­
мости в таких предположениях.

5 .8. Познакомьтесь с сnучаями некорректно й
работы прямой передачи
Одной из ярких звезд на небосклоне С++ 1 1 является прямая передача (perfect
forwarding). Можно сказать, идеально прямая. Но, как говорит наука, даже пространство
искривляется, так что есть идеальная прямота, а есть реальная. Прямая передача С++ 1 1
очень хороша, но достигает истинного совершенства, только если вы готовы игнориро­
вать небольшие искривления. В данном разделе вы познакомитесь с этими маленькими
искривлениями.
Перед тем как перейти к их изучению, стоит посмотреть, что мы подразумеваем
под "прямой передачей". Под передачей подразумевается, что одна функция передает
5 .8.

П ознакомьтесь с сnучаями некорректной работы прямой передачи

21 1

своим параметры другой функции. Цель этого действия заключается в том, чтобы одна
функция (которой передаются параметры) получила в точности те же объекты, которые
переданы другой функции (которая выполняет передачу). Тем самым исключается пере­
дача параметров по значению, поскольку при этом выполняется копирование исходно
переданных объектов; мы же хотим, чтобы передающая функция была способна рабо­
тать с изначально переданными объектами. Указатели также исключаются, поскольку мы
не хотим заставлять вызывающий код передавать указатели. Когда речь идет о передаче
общего назначения, мы работаем с параметрами, представляющими собой ссылки.
Прямая передача означает, что мы передаем не просто объекты, но и их основные
характеристики: их типы, являются они lvalue или rvalue, объявлены они как const или
vo la t i le. В сочетании с наблюдением о том, что мы будем иметь дело со ссылочными
параметрами, это означает, что мы будем использовать универсальные ссылки (см. раз­
дел 5.2), поскольку только параметры, являющиеся универсальными ссылками, хранят ин­
формацию о том, какие аргументы - являющиеся lvalue или rvalue - были им переданы.
Предположим, что у нас есть некоторая функция f и мы хотели бы написать функцию
(по правде говоря, шаблон функции), которая выполняет передачу ей. Ядро того, что нам
надо, имеет следующий вид:
template
void fwd ( T&& param)
{

11 Принимает любой аргумент

f ( std : : forward (param) ) ; / / Передача аргумента

в

f

Передающие функции по своей природе являются обобщенными. Шаблон fwd, на­
пример, принимает аргумент любого типа, и он передает все, что бы ни получил. Логи­
ческим продолжением этой обобщенности являются передающие функции, являющиеся
не просто шаблонами, а шаблонами с произвольным количеством аргументов (вариатив­
ными шаблонами (variadic templates)). Вариативная разновидность fwd выглядит следую­
щим образом:
template
void fwd ( Ts&& . . . params )

11 Принимает любые аргументы

{
f ( std : : forward ( pararns ) . . . ) ; / / Передача аргументов в f

Эту разновидность вы встретите, помимо прочих мест, в функциях размещения стан­
дартных контейнеров (см. раздел 8.2) и в фабричных функциях для интеллектуальных
указателей, s t d : : ma ke_shared и std : : ma ke_un ique (см. раздел 4.4).
Для заданной целевой функции f и нашей передающей функции fwd прямая передача
завершается неудачей, если вызов функции f с конкретным аргументом выполняет нечто
одно, а вызов fwd с теми же аргументами - нечто иное:

212

Глава 5. Rvаluе-ссылки, семантика перемещений и прямая передача

f ( express i on ) ;
/ / Если этот вызов выполняет что-то одно,
fwd ( expression ) ; / / а этот - нечто иное, прямая передача
/ / функцией fwd функции f неудачна

К такой неудаче могут привести несколько видов аргументов. Знать эти аргументы
и то, как с ними работать, весьма важно, так что начнем наш тур по аргументам, которые
не могут быть переданы с помощью прямой передачи.

Ин ици ализаторы в ф и rу р ных с кобках
Предположим, что f объявлена следующим образом:
void f ( const std : : vector& v ) ;

В этом случае вызов f с инициализаторами в фигурных скобках компилируется:
1 1 ОК, " { 1 , 2 , 3 } " неявно преобразуется
11 в s t d : : vector

f ( { l, 2, 3 }) ;

Однако передача того же инициализатора в фигурных скобках функции fwd не компи­
лируется:
fwd ( { 1 , 2 , 3 } ) ;

/ / Ошибка ! Код не компилируется 1

Дело в том, что применение инициализаторов в фигурных скобках - один из случаев,
когда прямая передача терпит неудачу.
Все такие случаи отказов имеют одну и ту же причину. В непосредственном вызове
f (таком, как f ( { 1 , 2 , 3 ) ) ) компиляторы видят аргументы, переданные в точке вызова,
и видят типы параметров, объявленные функцией f. Они сравнивают аргументы в точ­
ке вызова с объявлениями параметров на предмет совместимости и при необходимости
выполняют неявное преобразование, чтобы вызов был успешным. В приведенном выше
примере они генерируют временный объект типа s t d : : vect o r < i n t > из { 1 , 2 , 3 ) , так что
к параметру v функции f привязывается объект типа s t d : : vector.
При косвенном вызове f с помощью шаблона передающей функции fwd компиляторы
больше не сравнивают аргументы, переданные в точке вызова fwd, с объявлениями пара­
метров в f. Вместо этого они вь1водят типы аргументов, переданных в fwd, и сравнивают
выведенные типы с объявлениями параметров в f. Прямая передача оказывается неудач­
ной, если происходит что-то из следующего.


Компиляторы неспособны вывести тип

одного или нескольких параметров fwd.

В этом случае код не компилируется.


Компиляторы выводят "неверный" тип одного или нескольких параметров fwd.
Здесь "неверный" может означать как то, что инстанцирование fwd не компилиру­
ется с выведенными типами, так и то, что вызов f с использованием выведенных
типов fwd ведет себя не так, как непосредственный вызов f с аргументами, пере­
данными в fwd. Одним источником такого отклонения в поведении могла бы быть
ситуация, когда у f имеется перегрузка, и из-за "некорректного" вывода типов эта
перегрузка f, вызываемая в fwd, отличалась бы от перегруженной функции f, ис­
пользуемой при непосредственном вызове.

5.8.

Поэнакомыесь с случаями некорректной работы прямой передачи

213

В приведенном выше вызове " fwd ( { 1 , 2 , 3 } ) " проблема заключается в том, что пере­
дача инициализатора в фигурных скобках параметру шаблона функции, не объявленно­
му как std : : i n i t ia l i zer_ l i st, заставляет его быть, как предписывает стандарт, "не вы­
водимым контекстом': На простом человеческом языке это означает, что компиляторам
запрещено выводить тип для выражения { 1 , 2 , 3 } в вызове fwd, поскольку параметр fwd
не объявлен как std : : i n i t i a l i zer_l i s t . Не имея возможности вывести тип параметра
fwd, компиляторы, понятно, вынуждены отклонять такой вызов.
Интересно, что в разделе 1 .2 поясняется, что вывод типа для переменных auto, ини­
циализированных с помощью инициализатора в фигурных скобках, успешен. Такие пере­
менные считаются объектами типа s t d : : i n i t i a l i zer_ l i s t , и это обеспечивает простой
обходной путь для случаев, когда тип передающей функции должен быть выведен как
std : : i n i t i al i zer_ l i s t : объявить локальную переменную как auto, а затем передать ее
в передающую функцию:
auto il
fwd ( i l ) ;

=

{ 1,

2,

3 } ; // Тип il выводится как

11 s td : : initiali zer l ist
// ОК, прямая передача il в f

о и NULL в качес тве нулевых указателей
В разделе 3.2 поясняется, что, когда вы пытаетесь передать в шаблон О или NULL в каче­
стве нулевого указателя, вывод типа для переданного аргумента дает вместо типа указателя
целочисленный тип (обычно int ) . В результате ни О, ни NULL не может быть передано с по­
мощью прямой передачи как нулевой указатель. Решение проблемы простое: передавать
nullptr вместо О и NULL. Детальную информацию вы можете найти в разделе 3.2.

Целочи с лен ные члены-данные s tatic cons t
и cons texpr без определени й
В качестве общего правила не требуется определять в классах целочисленные члены­
данные stat i c const и constexpr; одних объявлений вполне достаточно. Дело в том, что
компиляторы выполняют распространение con s t для значений таких членов, тем самым
устраняя необходимость выделять для них память. Например, рассмотрим такой код:
class Widget {
puЫ i c :
/ / Объявление MinVals :
static constexpr std: : size_t МinVals

=

28;

);

1 1 Объявления MinVal s нет
std: : vector widgetDa t a ;
widgetData . reserve (Widget : : МinVals ) ; / / Использование MinVa ls

214

Гпава 5. Rvalue-ccыnки, сема н тика перемеще н ий и прямая передача

Здесь мы используем Widg e t : : M i nVa l s (далее - просто Mi nVa l s ) для указания на­
чальной емкости widgetData, даже несмотря на то, что определения M i nVa l s нет. Ком­
пиляторы обходят отсутствующее определение (как и должны это делать) подстановкой
значения 28 во все места, где упоминается MinVa l s . Тот факт, что для значения MinVa l s
не выделена память, проблемой н е является. Если берется адрес M i nVa l s (например, кто­
то создает указатель на M i nVa l s ) , то M inVa l s требует места в памяти (чтобы указателю
было на что указывать), и тогда приведенный выше код хотя и будет компилироваться,
не будет компоноваться до тех пор, пока не будет предоставлено определение M i nVa l s .
С учетом этого представим, что f (которой функция fwd передает аргумент) объявле­
на следующим образом:
void f ( std: : si ze_t val ) ;

Вызов f с M inVa ls проблемы не представляет, поскольку компиляторы просто заменяют
MinVa l s его значением:
11 ОК, рассматривается как " f ( 2 8 ) "

f (Widget : :МinVals) ;

Увы, все не так хорошо, если попытаться вызвать f через fwd:
fwd (Widget : :МinVals ) ;

/ / Ошибка ! Не должно компоноваться

Этот код компилируется, но не должен компоноваться. Если это напоминает вам про­
исходящее при взятии адреса MinVa l s, это хорошо, потому что проблема в обоих случаях
одна и та же.
Хотя нигде в исходном коде не берется адрес MinVa ls, параметром fwd является уни­
версальная ссылка, а ссылки в коде, сгенерированном компилятором, обычно рассматри­
ваются как указатели. В бинарном коде программы указатели и ссылки, по сути, пред­
ставляют собой одно и то же. На этом уровне можно считать, что ссылки - это просто
указатели, которые автоматически разыменовываются. В таком случае передача MinVals
по ссылке фактически представляет собой то же, что и передача по указателю, а раз так,
то должна иметься память, на которую этот указатель указывает. Передача целочислен­
ных членов-данных s t a t i c const и constexpr по ссылке в общем случае требует, чтобы
они были определены, и это требование может привести к неудачному применению пря­
мой передачи там, где эквивалентный код без прямой передачи будет успешен.
Возможно, вы обратили внимание на некоторые юркие слова, употребленные мною
выше. Я сказал, что код "не должен" компоноваться. Ссылки "обычно" рассматривают­
ся как указатели. Передача целочисленных членов-данных s t a t i c const и cons t expr
по ссылке "в общем случае" требует, чтобы они были определены. Это похоже на то, как
будто я что-то знаю, но ужасно не хочу вам сказать . . .
Это потому, что так и есть. В соответствии со стандартом передача MinVa l s по ссыл­
ке требует, чтобы этот член-данные был определен. Но не все реализации выполняют
это требование. Так что в зависимости от ваших компиляторов и компоновщиков вы
можете обнаружить, что в состоянии выполнить прямую передачу целочисленных чле­
нов-данных s t a t i c const и cons t expr, которые не были определены. Если это так поздравляю, но нет причин ожидать, что такой код будет переносим. Чтобы сделать его
5.8.

Познакомьтесь с сnучаями некорректной работы nрямой передачи

21 S

переносимым, просто добавьте определение интересующего вас целочисленного члена­
данных, объявленного как s t a t i c const или const expr. Для M i nVa l s это определение
имеет следующий вид:
constexpr std : : s i ze_t Widget : : MinVals ; / / В . срр- файле Widget

Обратите внимание, что в определении не повторяется инициализатор (28 в случае
MinVa l s ) . Не переживайте об этом. Если вы забудете предоставить инициализатор в обо­

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

И м ена пе р еr руженных функ ци й и и м ена шабл о н ов
Предположим, что поведение нашей функции f (которой мы хотим передать аргу­
менты через шаблон fwd) может настраиваться путем передачи ей некоторой функции,
выполняющей определенную работу. В предположении, что эта функция получает и воз­
вращает int, функция f может быть объявлена следующим образом:
void f ( int ( *pf) (int) ) ; // pf - функция обработки

Стоит заметить, что f может также быть объявлена с помощью более простого синтакси­
са без указателей. Такое объявление может имеет следующий вид, несмотря на то что оно
означает в точности то же, что и объявление выше:
void f ( int pf (int) ) ;

/ / Объявление той же f , что и вьШJе

В любом случае теперь предположим, что у нас есть перегруженная функция
processVa l:
int processVal ( int value ) ;
int processVal ( int value , int priority) ;

Мы можем передать processVal функции f,
f (processVa l ) ; / / Без проблем

но это выглядит удивительным. Функции f в качестве аргумента требуется указатель
на функцию, но processVal не является ни указателем на функцию, ни даже функцией;
это имя двух разных функций. Однако компиляторы знают, какая processVal нужна: та,
которая соответствует типу параметра функции f. Таким образом, они могут выбрать
proce ssVa l , принимающую один int, а затем передать адрес этой функции в функцию f.
Все это работает благодаря тому, что объявление f позволяет компиляторам выве­
сти требующуюся версию функции proces sVa l. Однако fwd представляет собой шаблон
функции, не имеющий информации о том, какой тип ему требуется, а потому компиля­
торы не в состоянии определить, какая из перегрузок должна быть передана:
fwd (processVa l ) ; / / Ошибка ' Какая processVal?

Само по себе имя processVa l не имеет типа. Без типа не может быть вывода типа,
а без вывода типа мы получаем еще один случай неудачной прямой передачи.

216

Глава 5 . Rvаl uе-ссылки, семантика перемещений и прямая передача

Та же проблема возникает и если мы пытаемся использовать шаблон функции вместо
перегруженного имени функции или в дополнение к нему. Шаблон функции представля­
ет не единственную функцию, он представляет множество функций:
template
Т workOnVal ( T param) / / Шаблон для обработки значений
{ ... }
fwd (workOnVal ) ;

1 1 Ошибка ! Какое инстанцирование workOnVal ?

Получить функцию прямой передачи наподобие fwd, принимающую имя перегружен­
ной функции или имя шаблона, можно, вручную указав перегрузку (или инстанцирова­
ние), которую вы хотите передать. Например, вы можете создать указатель на функцию
того же типа, что и параметр f, инициализировать этот указатель с помощью processVal
или workOnVal (тем самым обеспечивая выбор корректной версии processVal или гене­
рацию корректного инстанцирования workOnVal) и передать его шаблону fwd:
using Process FuncType =
int ( * ) ( i nt ) ;
ProcessFuncТype processValPtr

1 1 Псевдоним типа (раздел 3 . 3 )

1 1 Определяем необходимую
/ / сигнатуру proces sVal
11 ОК
fwd (processValPt r ) ;
fwd ( static cast (workOnVal ) ) ; / / Также ОК
_
processVal ;

Конечно, это требует знания типа указателя на функцию, передаваемого шаблоном
fwd. Разумно предположить, что функция с прямой передачей это документирует. В кон­
це концов, функции с прямой передачей предназначены для принятия всего что угодно,
так что если нет никакой документации, говорящей, что должно быть передано, то от­
куда вы узнаете, что им передавать?

Б итовые поn я
Последний случай неудачной прямой передачи - когда в качестве аргумента функции
используется битовое поле. Чтобы увидеть, что это означает на практике, рассмотрим за­
головок IPv4, который может быть смоделирован следующим образом:4
struct I Pv4Header
std: : uint32 t version : 4 ,
I Н1 : 4 ,
DSC P : 6 ,
ECN : 2 ,
totalLength : 1 6 ;
};
4 Здесь предполагается, что битовые поля располагаются о т младшего бита к старшему. С++ это
не гарантирует, но компипяторы часто предлагают механизм, который позволяет программисту
управпнть схемой размещения битовых попей.
S.8. Познакомьтесь с случаями некорректной работы л рямой передачи

217

Если наша многострадальная функция f (являющаяся целевой для нашей функции
прямой передачи fwd) объявлена как принимающая параметр std : : si ze _ t, то вызов,
скажем, с полем t o t a l Le ngth объекта I Pv 4 Header компилируется без проблем:
void f ( std: : s ize t s z ) ; 11 Вызываемая функция
I Pv4Header h ;
f ( h . totalLength ) ;

1 1 Все в порядке

Однако попытка передать h . tota lLength в f через fwd - это совсем другая история:
fwd ( h . totalLength ) ;

1 1 О1Ш1бка !

Проблема заключается в том, что параметр функции fwd представляет собой ссылку,
а h . t ot а l Lengt h - неконстантное битовое поле. Это может показаться не столь уж пло­
хим, но стандарт С++ осуждает это сочетание в непривычно ясной форме: "неконстант­
ная ссылка не может быть привязана к битовому полю': Тому есть превосходная причина.
Битовые поля могут состоять из произвольных частей машинных слов (например, биты
3-5 из 32-битного int ) , но непосредственно их адресовать нет никакой возможности. Ра­
нее я упоминал, что ссылки и указатели представляют собой одно и то же на аппаратном
уровне, и просто так же, как нет никакого способа создать указатели на отдельные биты
(С++ утверждает, что наименьшей сущностью, которую вы можете адресовать, является
char) , нет никакого способа связать ссылку с произвольными битами.
Обходной путь для невозможности прямой передачи битовых полей становится прос­
тым, как только вы осознаете, что любая функция, принимающая битовое поле в качес­
тве аргумента, на самом деле получает копию значения битового поля. В конце концов,
никакая функция не может привязать ссылку к битовому полю, поскольку не существует
указателей на битовые поля. Единственная разновидность параметров, которым могут
передаваться битовые поля, - это параметры, передаваемые по значению, и, что инте­
ресно, ссылки на const. В случае передачи по значению вызываемая функция, очевид­
но, получает копию значения в битовом поле, и оказывается, что в случае параметра,
являющегося ссылкой на const, стандарт требует, чтобы эта ссылка в действительности
была привязана к копии значения битового поля, сохраненного в объекте некоторого
стандартного целочисленного типа (например, i nt ). Ссылки на const не привязываются
к битовым полям, они привязываются к "нормальным" объектам, в которые копируются
значения битовых полей.
Итак, ключом к передаче битового поля в функцию с прямой передачей являет­
ся использование того факта, что функция, в которую осуществляется передача, всег­
да получает копию значения битового поля. Таким образом, вы можете сделать копию
самостоятельно и вызвать передающую функцию, передав ей копию. В нашем примере
с I Pv4Header код, осуществляющий этот подход, имеет следующий вид:
11 Копирование значения битового поля; см . раздел 2 . 2
auto length
static_cast ( h . tota lLength ) ;
fwd ( l ength) ;
11 Передача копии
=

218

Гnава 5 . Rvalue-cc ыn ки, семантика перемещений и прямая передача

Резюм е
В большинстве случаев прямая передача работает так, как разрекламировано. Вы ред­
ко должны задумываться о ней. Но когда она не работает (разумно выглядящий код не
компилируется или, хуже того, компилируется, но работает не так, как вы ожидаете),
важно знать о несовершенствах прямой передачи. Не менее важно знать, как эти несо­
вершенства обойти. В большинстве случаев это довольно просто.
Сnедует запомнить


Прямая передача неудачна, когда вывод типа не удается выполнить или когда он вы­
водит неверный тип.



Разновидностями аргументов, которые приводят к неудачам при прямой передаче,
являются инициализаторы в фигурных скобках, нулевые указатели, выраженные
как О или NULL, целочисленные члены-данные, объявленные как const s t a t i c и не
имеющие определений, имена шаблонов и перегруженных функций и битовые поля.

S.8. Познакомьтесь с случаями некорректной работы прямой передачи

219

ГЛАВА 6

Л ямбда- вы ра жен ия

Лямбда-выражения, иногда называемые просто лямбдами (lambdas), существенно из­
менили правила игры в программировании на С++. Это несколько удивительно, так как
они не внесли в язык никаких новых возможностей выражения идей. Все, на что способ­
ны лямбда-выражения, вы в состоянии сделать вручную, разве что ценой немного боль­
ших усилий по вводу с клавиатуры. Но лямбда-выражения представляют собой очень
удобное средство создания функциональных объектов, оказывающее огромное влияние
на повседневную разработку программного обеспечения на С++. Без лямбда-выраже­
ний алгоритмы "_i f" из STL (например, s t d : : find_i f, s t d : : remove_ i f, s t d : : count_i f
и др.) обычно работают с самыми тривиальными предикатами, н о при доступности
лямбда-выражений применение этих алгоритмов резко возрастает. То же самое верно
и для алгоритмов, настраиваемых с помощью пользовательской функции сравнения (на­
пример, s t d : : sort , std : : nth_element, std : : lower_bound и др.). Вне STL лямбда-выра­
жения позволяют быстро создавать пользовательские удалители для std : : unique_pt r
и s t d : : sha red_pt r (см. разделы 4. l и 4.2) и делают столь же простыми спецификации
предикатов для переменных условий в потоковом API (см. раздел 7.5). Помимо исполь­
зования в объектах стандартной библиотеки, лямбда-выражения облегчают определение
функций обратного вызова "на лету", функций адаптации интерфейса и контекстно-за­
висимых функций для разовых вызовов. Лямбда-выражения действительно делают С++
более приятным языком программирования.
Терминология, связанная с лямбда-выражениями, может обескуражить. Лямбда-вы­
ражение является именно тем, что написано: выражением. Это часть исходного текста.
В приведенном ниже фрагменте выделенная полужирным шрифтом часть представляет
собой лямбда-выражение.
std : : find_i f ( con tainer. begin ( ) , container. end ( ) ,
[ ] (int val) { retшn О < val && val < 10 ; } ) ;


Замь1кание (closure) представляет собой объект времени выполнения, создаваемый
лямбда-выражением. В зависимости от режима захвата замыкания хранят копии
ссылок на захваченные данные. В приведенном выше вызове std : : find_i f замыка­
ние представляет собой объект, передаваемый во время выполнения в std : : find_ i f
в качестве третьего аргумента.



Класс замыкания (closure class) представляет собой класс, из котороrо инстанциру­
ется замыкание. Каждое лямбда-выражение заставляет компиляторы генерировать
уникальный класс замыкания. Инструкции внутри лямбда-выражения становятся
выполнимыми инструкциями функций-членов их класса замыкания.

Лямбда-выражения часто используются для создания замыкания, которое применя­
ется только в качестве аргумента функции. Именно этот случай представлен в приведен­
ном выше примере std : : find_ if. Однако в общем случае замыкания могут копировать­
ся, так что обычно можно иметь несколько замыканий типа замыкания, соответствую­
щего одному лямбда-выражению. Например, в коде
11 х - локальная переменная

int х ;
auto cl
[ х ] ( int у ) {
return х
cl;
auto с2
с2;
auto сЗ
=

*

11
11
у > 5 5 ; ) ; 11
11
11

c l - копия замыкания ,
сгенерированного
лямбда-выражением
с2
копия cl
сЗ - копия с2

и сЗ являются копиями замыкания, порожденного лямбда-выражением.
Неформально вполне приемлемо размытие границы между лямбда-выражениями, за­
мыканиями и классами замыканий. Но в последующих разделах очень часто важно раз­
личать, что существует во время компиляции (лямбда-выражения и классы замыканий),
что - во время выполнения (замыкания) и как они соотносятся одно с другим.
cl, с2

6.1 . Избеrайте режимов захвата по умопчанию
В С++ 1 1 имеются два режима захвата: по ссылке и по значению. Захват по умолчанию
по ссылке может привести к висячим ссылкам. Захват по умолчанию по значению со­
блазняет считать, что вы не подвержены этой проблеме (это не так), и думать, что ваши
замыкания являются самодостаточными автономными замыканиями (но они могут и не
быть таковыми).
Так выглядят основные положения данного раздела. Если вы в большей степени тво­
рец, чем ремесленник, вы, вероятно, захотите узнать побольше - так давайте начнем
с опасности захвата по ссылке.
Захват по ссылке приводит к тому, что замыкание содержит ссылку на локальную
переменную или на параметр, доступный в области видимости, где определено лямбда­
выражение. Если время жизни замыкания, созданного из лямбда-выражения, превышает
время жизни локальной переменной или параметра, ссылка в замыкании оказывается
висячей. Предположим, например, что у нас есть контейнер с функциями фильтрации,
каждая из которых принимает int и возвращает bool, указывающий, удовлетворяет ли
переданное значение фильтру:
222

Гnава 6. Лямбда-выражения

11 См. using в разделе 3 . 3 , std: : function - в 2 . 1 :
using FilterContainer = std : : vector;
FilterContainer filters; / / Функции фильтрации

Мы можем добавить фильтр для чисел, кратных 5, следующим образом:
filters . ernplace_back (
/ / См . ernplace_back в разделе 8 . 2
[ ] ( int value ) { return value % 5 == О ; }
);

Однако может быть так, что нам нужно вычислять делитель во время выполнения,
т.е. мы не можем жестко "прошить" значение 5 в лямбда-выражении. Поэтому добавле­
ние фильтра может выглядеть следующим образом:
void addDivi sorFilter ( )
auto calcl = cornputeSorneValuel ( ) ;
auto calc2
cornputeSorneValue2 ( ) ;
auto divisor
cornputeDivisor ( calc l , calc2 ) ;
filters . ernplace_back ( / / Опасно ! Ссылка на divi sor повиснет !
[&] ( int value ) { return value % divisor == О ; }
) ;
=

=

Этот код - бомба замедленного действия. Лямбда-выражение ссылается на ло­
кальную переменную divi sor, но эта переменная прекращает свое существование по­
сле выхода из addDivi sorFi l te r. Этот выход осуществляется сразу после выхода из
f i l ters . ernplace_back, так что добавленная в f i lters функция, по сути, является мерт­
ворожденной. Применение этого фильтра ведет к неопределенному моменту практиче­
ски с момента создания.
Та же проблема имеет место и при явном захвате di visor по ссылке,
filters . ernplace_back (
[ &divisor] ( int va lue )
{ return value % divisor
) ;

11 Опасно ! Ссылка на
О ; ) // divisor все равно
11 повисает !

но при явном захвате проще увидеть, что жизнеспособность лямбда-выражения зависит
от времени жизни di vi sor. Кроме того, использование имени di visor напоминает нам
о необходимости убедиться, что di vi sor существует как минимум столько же времени,
сколько и замыкание лямбда-выражения. Это более конкретное напоминание, чем обоб­
щенное "убедитесь, что ничего не висит", о чем гласит конструкция [ & ] .
Если вы знаете, что замыкание будет использовано немедленно (например, будучи
переданным в алгоритм STL) и не будет копироваться, нет никакого риска, что ссылки,
которые оно хранит, переживут локальные переменные и параметры в среде, где созда­
но это лямбда-выражение. Вы могли бы утверждать, что в этом случае нет риска полу­
чить висячие ссылки, а следовательно, нет причин избегать режима захвата по ссылке

6.1 .

Избегайте режимов захвата по умопчанию

223

по умолчанию. Например, наше фильтрующее лямбда-выражение может использоваться
только как аргумент в алгоритме С++ 1 1 s t d : : a l l _ of, который проверяет, все ли элемен­
ты диапазона удовлетворяют условию:
template
void workWithContainer ( const С& container)
auto calcl
computeSomeValue l ( ) ;
11 Как и ранее
11 Как и ранее
auto calc2
computeSomeVal ue2 ( ) ;
auto clivisor
computeDivisor ( ca l c l , calc2 ) ; / / Как и ранее
=

=

=

11 Тип элементов в контейнере :

using ContElemT

=

typename C : : value type ;

using std : : begin;
using std : : end;

11 Для обобщенности ;
11 см. раздел 3 . 7

i f ( std: : all_of (
11
begin ( container ) ,
11
end ( container) ,
11
[ &] ( const ContElemT& value )
{ return value % divisor
) {
11
else {

Все значения
в контейнере
кратны divisor?

О; ) )
Да

11 Как минимум одно - нет

Да, все так, это безопасно, но эта безопасность довольно неустойчива. Если выяснит­
ся, что это лямбда-выражение полезно в друтих контекстах (например, как функция, до­
бавленная в контейнер f i l t ers) и будет скопировано и вставлено в контекст, где это
замыкание может пережить di v i s o r, вы вновь вернетесь к повисшим ссылкам, и в выра­
жении захвата не будет ничего, что напомнило бы вам о необходимости анализа времени
жизни di vi s or .
С точки зрения долгосрочной перспективы лучше явно перечислять локальные пере­
менные, от которых зависит лямбда-выражение.
Кстати, возможность применения auto в спецификации параметров лямбда-выраже­
ний в С++ 1 4 означает, что приведенный выше код можно записать проще при исполь­
зовании С++ 1 4. Определение псевдонима типа Con t E l emT можно убрать, а условие i f
может быть переписано следующим образом:
if ( std : : all_of (begin ( container ) , end ( container ) ,
[ & ] ( const auto& value )
О; ) ) )
{ return value % divisor
==

224

Гла ва 6. Лямбда-выражения

!/ C++ l 4

Одним из способов решения нашей проблемы с локальной переменной divi s o r мо­
жет быть применение режима захвата по умолчанию по значению, т.е. мы можем доба­
вить лямбда-выражение к f i l t e r s следующим образом:
filters . emplace_back (
[=] ( iпt value )
{ return value % divisor
);

О; }

11
11
11
11

Теперь

divisor
н е может
зависнуть

Для данного примера этого достаточно, но в общем случае захват по умолчанию
по значению не является лекарством от висящих ссылок, как вам могло бы показаться.
Проблема в том, что если вы захватите указатель по значению, то скопируете его в замы­
кания, возникающие из лямбда-выражения, но не сможете предотвратить освобождение
объекта, на который он указывает (и соответственно, повисания), внешним кодом.
"Этого не может случиться! - возразите вы. - После прочтения главы 4 я работаю
только с интеллектуальными указателями. Обычные указатели используют только несчаст­
ные программисты на С++98': Это может быть правдой, но это не имеет значения, потому
что на самом деле вы используете обычные указатели, а они могут быть удалены. Да, в со­
временном стиле программирования на С++ в исходном коде это незаметно, но это так.
Предположим, что одна из задач, которые могут решать W i dget,
добавление эле­
ментов в контейнер фильтров:
-

class Widget
puЫi c :
11 Конструкторы и т . п .
void addFilter ( ) const ; / / Добавление элемента в filters
private :
int divisor ;
11 Используется в фильтре

};
W idget : : a ddF i l t e r может быть определен следующим образом:
void Widget : : addFilter ( ) const

{
filters . emplace_ba ck (
[=] ( int value ) { return value % divisor
);

О; }

Для блаженно непосвященных код выглядит безопасным. Лямбда-выражение зависит
от di v i s or, но режим захвата по умолчанию по значению гарантирует, что di v i s o r ко­
пируется в любое замыкание, получающееся из лямбда-выражения, так ведь�
Нет. Совершенно не так. Ужасно не так! Смертельно не так!
Захваты применяются только к нестатическим локальным переменным (включая
параметры), видимым в области видимости, в которой создано лямбда-выражение.
В теле W i dg e t : : addFi l t e r переменная d iv i s o r не является локальной переменной,

6.1 . Иэбеrайте режимов эахвата по умол чанию

225

это - член-данные класса Widget. Она не может быть захвачена. Если отменить режим
за.хвата по умолчанию, код компилироваться не будет:
void Widget : : addFilter ( ) const
{
filters . emplace_back (
/ / Ошибка ! divisor недоступна !
[ ] ( int value) { return value % divisor
О; }
);
==

Кроме того, если сделана попытка явного захвата divisor (по значению или по ссылке значения не имеет), за.хват не компилируется, поскольку divisor не является локальной
переменной или параметром:
void Widget : : addFilter ( ) const
{
f i lters . emplace_back (
// Ошибка ! Нет захватываемой
[divisor] ( int value ) / / локальной переменной divisor !
О; }
{ return value % divisor
);
==

Если захват по умолчанию по значению не захватывает d i v i sor, а без захвата
по умолчанию по значению код не компилируется, то что же происходит?
Объявление связано с неявным использованием обычного указателя: this. Каждая
нестатическая функция-член получает указатель t h i s, и вы используете этот указа­
тель всякий раз при упоминании члена-данных этого класса. В любой функции-члене
Widget, например, компиляторы внутренне заменяют каждое использование di visor
на this>di visor. В версии W idget : : addFi lter с за.хватом по умолчанию по значению
void Widget : : addFi lter ( ) const
{
filters . emplace_back (
[=] ( int value ) { return value % divisor
)

О; }

;

this>divisor захватывается указатель this объекта Widget, а не divi sor. Компиляторы
рассматривают этот код так, как будто он написан следующим образом:
void Widget : : addFi lter ( ) const
{
auto currentOЬjectptr = this;
fi lters . emplace_back (
[currentOЬjectptr] ( int value )
( return value % currentOЬjectptr->divisor
);

226

Глава 6. Лямбда-выражения

О; }

Понимание этого равносильно пониманию того, что жизнеспособность замыканий,
вытекающих из этого лямбда-выражения, связана со временем жизни объекта Widget,
копии указателя this которого в них содержатся. В частности, рассмотрим код, который
в соответствии с главой 4 использует только интеллектуальные указатели:
using FilterContainer =
11 Как и ранее
std : : vector ;
1 1 Как и ранее

FilterContainer filters ;

void doSomeWork ( )
{
auto pw
11
std : : ma ke unique ( ) ; / /
11
pw- >addFi lter ( ) ;
11
11
=

Создание Widget ;

s td : : ma ke_unique см . в
разделе 4 . 4
Добавление фильтра
с Widget : : divisor

1 1 Уничтожение Widget ; fil ters хранит висячий указатель !

Когда выполняется вызов doSomeWork, создается фильтр, зависящий от объекта Widget,
созданного std : : make_unique, т.е. фильтр, который содержит копию указателя на этот
Widget, - указатель t h i s объекта Widget. Этот фильтр добавляется в f i l ters, но по за­
вершении работы doSomeWor k объект W i dget уничтожается, так как st d : : un i que_ptr
управляет его временем жизни (см. раздел 4.1 ). С этого момента f i lt ers содержит элемент
с висячим указателем.
Эта конкретная проблема может быть решена путем создания локальной копии чле­
на-данных, который вы хотите захватить, и захвата этой копии:
void Widget : : addFilter ( ) const
{
auto divisorCopy = divisor ;

filter s . emplace_back (
[divisorCopy] ( int value )
{ return value % divisorCopy
);

О;

1

11
11
11
11

Копирование
члена-данных
Захват копии
Е е использование

Чтобы быть честным, скажу, что при таком подходе захват по умолчанию по значению
также будет работать:
void Widget : : addFilter ( ) const
{
auto divisorCopy = divisor ;

filters . emplace_back (
[=] ( int value )
{ return value % divisorCopy
);

О;

11
11
11
1 11

Копирование
члена-данных
Захват копии
Е е использование

6 . 1 . Избегайте режимов захвата по умол чанию

227

но зачем искушать судьбу? Режим захвата по умолчанию делает возможным случайный
захват t h i s, когда вы думаете, в первую очередь, о захвате di vi sor.
В С++ 14 имеется лучший способ захвата члена-данных, заключающийся в использо­
вании обобщенного захвата лямбда-выражения (см. раздел 6.2):
void Widget : : addFilter ( ) const
{
f ilters . emplace_back (
[divisor = divisor] ( int value )
( return value % divisor
)

==

О;

//
11
11
11

С++1 4 :
Копирование divi sor
в замыкание
Использование копии

;

Однако такого понятия, как режим захвата по умолчанию для обобщенного захвата
лямбда-выражения, не существует, так что даже в С++ 14 остается актуальным совет дан­
ного раздела - избегать режимов захвата по умолчанию.
Дополнительным недостатком режимов захвата по умолчанию является то, что они
могут предполагать самодостаточность соответствующих замыканий и их изолирован­
ность от изменений внешних данных. В общем случае это не так, поскольку лямбда-вы­
ражения могут зависеть не только от локальных переменных и параметров (которые мо­
гут быть захвачены), но и от объектов со статическим временем хранения. Такие объек­
ты определены в глобальной области видимости или области видимости пространства
имен или объявлены как s t a t i c внутри классов, функций или файлов. Эти объекты мо­
гут использоваться внутри лямбда-выражений, но не могут быть захвачены. Тем не менее
спецификация режима захвата по умолчанию может создать именно такое впечатление.
Рассмотрим преобразованную версию функции addDivi sorFi lt er, с которой мы встре­
чались ранее:
void addDivisorFi l ter ( )
static auto calcl
=

1 1 Статический

computeSomeValuel ( ) ;

static auto ca lc2
=

11 Статический

computeSomeValue2 ( ) ;

static auto divisor

=

11 Статический

computeDivisor ( calcl , calc2 ) ;
fi lters . emplace_back (
[=] ( int value )
{ return value % divisor
);
++divisor;

228

Глава 6. Лямбда-выражения

1 1 Ничего н е захватывает
О ; } / / Ссылка на статическую
1 1 переменную
1 1 Изменение divisor

Случайный читатель этого кода может быть прощен за то, что, видя [ = ] может по­
думать "Отлично, лямбда-выражение делает копию всех объектов, которые использует,
и поэтому оно является самодостаточным': Но это не так. Это лямбда-выражение не ис­
пользует никакие нестатические локальные переменные, поэтому ничего не захватывает­
ся. Вместо этого код лямбда-выражения обращается к статической переменной di vi sor.
Когда в конце каждого вызова addDi v i s o r F i l t e r выполняется увеличение di vi sor, все
лямбда-выражения, которые были добавлены в f i l ters с помощью данной функции, бу­
дут демонстрировать новое поведение (соответствующее новому значению d iv is o r ) . С
практической точки зрения это лямбда-выражение захватывает divisor по ссылке, а это
выглядит противоречащим объявленному захвату по умолчанию по значению. Если дер­
жаться подальше от захвата по умолчанию по значению, можно уменьшить риск невер­
ного понимания такого кода.
,

Следует запомнить


Захват по умолчанию по ссылке может привести к висячим ссылкам.



Захват по умолчанию по значению восприимчив к висячим указателям (особенно
к this ) и приводит к ошибочному предположению о самодостаточности лямбда-вы­
ражений.

6.2. Испопьзуйте инициапизирующи й захват
дпя переме щения объектов в замыкания
Иногда ни захват по значению, н и захват по ссылке не является тем, что вы хо­
тите. Если у вас имеется объект, который можно только перемещать (например,
std : : unique_ptr или std : : future ) и который вы хотите передать замыканию, С++ 1 1 не
предлагает вам никакого способа для этого. Если у вас есть объект, который гораздо де­
шевле переместить, чем копировать (например, большинство контейнеров стандартной
библиотеки), и вы хотели бы передать его в замыкание, то гораздо эффективнее пере­
местить его, чем копировать. И вновь С++ 1 1 не предоставляет вам способа сделать это.
Но только C++ l l . С++ 1 4 - совершен но другая история. Он предлагает непосред­
ственную поддержку перемещения объектов в замыкания. Если ваш компилятор соот­
ветствует стандарту С++ 1 4, радуйтесь и читайте дальше. Если же вы работаете с компи­
ляторами С++ 1 1, вы тоже должны радоваться и читать дальше - потому что и в С++ 1 1
имеются способы приблизиться к перемещающему захвату.
Отсутствие перемещающего захвата было признано недостатком даже при приня­
тии C++ l l . Казалось бы, простейшим путем было его добавление в С++ 14, но Комитет
постандартизации пошел иным путем. Он добавил новый механизм, который настоль­
ко гибкий, что захват путем перемещения является всего лишь одним из вариантов его
работы. Новая возможность называется инициализирующим захватом (init capture). Он
может делать почти все, что могут делать захваты в С++ 1 1, и еще многое. Единственное,
что нельзя выразить с помощью инициализирующего захвата (и от чего, как поясняется
6.2. Используйте инициализирующий захват для перемещения объектов в замыкания

229

в разделе 6. 1 , вам надо держаться подальше), - это режим захвата по умолчанию. (Для
ситуаций, охватываемых захватами С++ 1 1 , инициализирующий захват несколько много­
словнее, так что там, где справляется захват С++ 1 1 , совершенно разумно использовать
именно его.)
Применение инициализирующего захвата делает возможным указать
1.

имя члена-данных

в классе замыкания, сгенерированном из лямбда-выражения, и

2.

выражение инициализации

этого члена-данных.

Вот как можно использовать и н ициализирующий захват для перемещения
std : : un i que_ptr в замыкание:
1 1 Некоторый полезный тип

class Widget
puЫ i c :
bool i sValidated ( ) const;
bool isProcessed ( ) cons t ;
bool isArchived ( ) con s t ;
priva te :
};
auto pw
s td : : make_unique ( ) ;
=

11
11
11
11
auto fuпc
[pw
std: : move (pw) ] 1 1
{ return pw- >isValidated ( )
11
& & pw- >isArchived ( ) ; } ; 1 1
=

=

Создание Widge t ;
std : : make_unique
см . в разделе 4 . 4
Настройка •pw
Инициализация члена
в замыкании с помощью
s t d : : move ( pw)

Выделенный текст представляет собой инициализирующий захват. Слева от знака
равенства = находится имя члена-данных в классе замыкания, который вы определяе­
те, а справа - инициализирующее выражение. Интересно, что область видимости слева
от "=" отличается от области видимости справа. Область видимости слева - это область
видимости класса замыкания. Область видимости справа - та же, что и определяемого
лямбда-выражения. В приведенном выше примере имя pw слева от = ссылается на чле­
ны-данные в классе замыкания, в то время как имя pw справа ссылается на объект, объ­
явленный выше лямбда-выражения, т.е. на переменную, инициализированную вызовом
std : : ma ke_u п i que. Так что "pw
s t d : : move ( pw ) " означает "создать член-данные pw
в замыкании и инициализировать этот член-данные результатом применения std : : move
к локальной переменной pw':
Как обычно, код в теле лямбда-выражения находится в области видимости класса за­
мыкания, так что использованные в нем pw относятся к члену-данным класса замыкания.
Комментарий "настройка * pw" в этом примере указывает, что после созда­
ния W i dget с помощью s t d : : ma ke_u n i que и до того, как интеллектуальный указа­
тель s t d : : u n i que_pt r на этот W i d g e t будет захвачен лямбда-выражением, W i dget
=

230

Глава 6. Лямбда-выражения

некоторым образом модифицируется. Если такая настройка не нужна, т.е. если объект
Wi dget , созданный с помощью s t d : : ma ke_unique, находится в состоянии, приrодном

для захвата лямбда-выражением, локальная переменная pw не нужна, поскольку член­
данные класса замыкания может быть непосредственно инициализирован с помощью
std : : make_unique:
auto func = [pw = std.: : make_unique ( ) ] / / Инициализация
{ return pw- >isValidated ( ) / / члена -данных в замыкании
&& pw- >i sArchived ( ) ; } ;
/ / результатом вызова make_unique

Из этоrо должно быть ясно, что понятие захвата в С++ 14 значительно обобщено
по сравнению с С++ 1 1 , поскольку в С++ 1 1 невозможно захватить результат выражения.
Поэтому еще одним названием инициализирующеrо захвата является обобщенный за­
хват лямбда-выражения (generalized lambda capture).
Но что если один или несколько используемых вами компиляторов не поддерживают
инициализирующий захват С++ 14? Как выполнить перемещающий захват в языке, в ко­
тором нет поддержки перемещающеrо захвата?
Вспомните, что лямбда-выражение - это просто способ rенерации класса и создания
объекта этоrо типа. Нет ничеrо, что можно сделать с лямбда-выражением и чеrо нельзя
было бы сделать вручную. Например, код на С++ 1 4, который мы только что рассмотрели,
может быть записан на С++ 1 1 следующим образом:
class I sValAndArch
puЫic :
using DataType

std : : unique_ptr ;

explicit IsValAndArch ( DataType & &ptr) / / Применение std : :move
/ / описано в разделе 5 . 3
: pw ( std : : move (ptr ) ) { }
bool operator ( ) ( ) const
{ return pw->isValidated ( ) && pw- >isArchived ( ) ; }
private :
DataType pw ;
};
auto func

=

IsVa lAndArch ( s td : : make_unique ( ) ) ;

Это требует больше работы, чем написание лямбда-выражения, но это не меняет тоrо
факта, что если вам нужен класс C++ l l, поддерживающий перемещающую инициализа­
цию своих членов-данных, то ваше желание отделено от вас лишь некоторым временем
за клавиатурой.
Если вы хотите придерживаться лямбда-выражений (с учетом их удобства это, веро­
ятно, так и есть), то перемещающий захват можно эмулировать в С++ 1 1 с помощью
1.

перемещения захватываемого объекта в функциональный объект с помощью
std : : Ьind

2.

и

передачи лямбда-выражению ссылки на захватываемый объект.

6.2.

Используйте инициа л изирующий захват для перемещения объектов в замыкания

231

Если вы знакомы с s t d : : b iпd, код достаточно прост. Если нет, вам придется немного
привыкнуть к нему, но игра стоит свеч.
Предположим, вы хотите создать локальный std : : vector, разместить в нем соответ­
ствующее множество значений, а затем переместить его в замыкание. В С++ 14 это просто:
1 1 Объект , перемещаемый
1 1 в замыкание
11 Наполнение данными
[ data = std: : move (data) ] / / Инициализирующий захват
{ /* Использование данных */ ) ;

std: : vector data;

auto fuпc

Я выделил ключевые части этого кода: тип объекта, который вы хотите перемещать
( s t d : : vector), имя этого объекта (data) и выражение инициализации для ини­

циализирующего захвата ( s t d : : rnove { d a t a ) ). Далее следует эквивалент этого кода
на С++ 1 1 , в котором я выделил те же ключевые части:
std : : vector da ta ;

// Как и ранее
11 Как и ранее

auto func
std : : bind (
/ / Эмуляция в С++ 1 1
[ ] ( const std : : vector& data) / / инициализирующего
11 захвата
{ / * Использование данных * / ) ,
=

std : : move (data)
)

;

Подобно лямбда-выражениям, std : : Ьiпd создает функциональные объекты. Я назы­
ваю функциональные объекты, возвращаемые std : : Ьiпd, Ьiпd-объектами. Первый ар­
гумент st d : : Ь i nd - вызываемый объект. Последующие аргументы представляют пере­
даваемые этому объекту значения.
Вind-объект содержит копии всех аргументов, переданных s t d : : Ьind. Для каждого
lvаluе-аргумента соответствующий объект в Ьind-объекте создается копированием. Для
каждого rvalue он создается перемещением. В данном примере второй аргумент пред­
ставляет собой rvalue (как результат применения std : : rnove; см. раздел 5. 1 ), так что dat a
перемещается в Ьiпd-объект. Это перемещающее создание является сутью эмуляции пе­
ремещающего захвата, поскольку перемещение rvalue в Ьiпd-объект и есть обходной путь
для перемещения rvalue в замыкание С++ 1 1 .
Когда Ьind-объект "вызывается" (т.е. выполняется его оператор вызова функции), со­
храненные им аргументы, первоначально переданные в s t d : : Ьi пd, передаются в вызы­
ваемый объект. В данном примере это означает, что когда вызывается Ьind-объект fuпc,
лямбда-выражению, переданному в std : : Ьiпd, в качестве аргумента передается создан­
ная в fuпc перемещением копия data.
Это лямбда-выражение то же самое, что и использованное нами в C++ l4, за исклю­
чением добавленного параметра data. Этот параметр представляет собой lvalue-ccылкy
на копию data в Ьind-объекте. (Это не rvalue-ccылкa, поскольку, хотя выражение, ис­
пользованное для инициализации копии data (" std : : rnove ( da t a ) "), является rvalue, сама
по себе копия dat a представляет собой lvalue.) Таким образом, применение data внутри
232

Гnава 6. Лямбда-выражения

лямбда-выражения будет работать с копией data внутри Ьiпd-объекта, созданной пере­
мещением.
По умолчанию функция-член operator ( ) в классе замыкания, сгенерированном из
лямбда-выражения, является const. Это приводит к тому, что все члены-данные в замы­
кании в теле лямбда-выражения являются константными. Однако созданная перемеще­
нием копия data внутри Ьiпd-объекта не является константной, так что, чтобы предот­
вратить модификацию этой копии data внутри лямбда-выражения, параметр лямбда-вы­
ражения объявляется как указатель на const. Если лямбда-выражение было объявлено
как mutaЫ е, operator ( ) в его классе замыкания не будет объявлен как const, так что
целесообразно опустить const в объявлении параметра лямбда-выражения:
auto func
1 1 Эмуляция в C++ l l
std : : bind (
[ ] (std: : vector& dat a ) mutaЫe 1 1 инициализирующего
1 1 захвата для лямбда­
{ /* uses of data * / f ,
/ / выражения, объяв­
std : : move (da t a )
// ленного mutaЫe
);
=

Поскольку Ьind-объект хранит копии всех аргументов, переданных s t d : : Ьi nd, Ьind­
объект в нашем примере содержит копию замыкания, произведенного из лямбда-выра­
жения, являющегося первым аргументом этого объекта. Следовательно, время жизни за­
мыкания совпадает со временем жизни Ьind-объекта. Это важно, поскольку это означает,
что, пока существует замыкание, существует и Ьind-объект, содержащий объект, захва­
ченный псевдоперемещением.
Если вы впервые столкнулись с std : : Ь i nd, вам может понадобиться учебник или
справочник по C++l I, чтобы все детали этого обсуждения встали на свои места в вашей
голове. Вот основные моменты, которые должны быть понятными.


Невозможно выполнить перемещение объекта в замыкание С++ 1 1, но можно вы­
полнить перемещение объекта в Ьind-объект С++ 1 1 .



Эмуляция захвата перемещением в С++ 1 1 состоит в перемещении объекта в Ьind­
объект с последующей передачей перемещенного объекта в лямбда-выражение
по ссылке.



Поскольку время жизни Ьind-объекта совпадает с таковым для замыкания, можно
рассматривать объекты в Ьind-объекте так, как будто они находятся в замыкании.

В качестве второго примера применения std : : Ь i nd для эмуляции перемещающего
захвата рассмотрим пример кода С++ 1 4, который мы видели ранее и который создает
std : : unique_ptr в замыкании:
auto func
[pw
std: : make_unique ( ) ] / / Как и ранее,
/ / создает pw
{ return pw - >i sVa lidated ( )
& & pw->isArchived ( ) ; ) ;
1 1 в замыкании
=

=

А вот как выглядит его эмуляция на С++ 1 1:

6.2.

Испоnьзуйте инициаnизирующий захват для перемещения объектов в замыкания

233

auto func

=

std : : bind (

[ ] ( const std: : unique_ptr& pw)
{ re turn pw - >isValidated ( )
& & pw->isArchived ( ) ; } ,
std: : make_unique ( )
)

;

Забавно, что я показываю, как использовать s t d : : Ьind для обхода ограничений лямб­
да-выражений в С++ 1 1 , поскольку в разделе 6.4 я выступаю как сторонник применения
лямбда-выражений вместо s t d : : Ьind. Однако в данном разделе поясняется, что в С++ 1 1
имеются ситуации, когда может пригодиться s t d : : Ь i nd, и это одна из них. (В С++14 та­
кие возможности, как инициализирующий захват и параметры auto, устраняют такие
ситуации.)
Сnедует запомнить


Для перемещения объектов в замыкания используется инициализирующий захват
С++14.



В С++ 1 1 инициализирующий захват эмулируется с помощью написания классов
вручную или применения s t d : : Ьi nd.

6.3 . Испопьзуйте параметры decl type дпя auto & &
дпя передачи с помощью s td : : forward
Одной из самых интересных возможностей С++ 14 являются обобщенные лямбда-вы­
ражения - лямбда-выражения, в спецификации параметров которых используется клю­
чевое слово auto. Реализация этой возможности проста: operator ( ) в классе замыкания
лямбда-выражения является шаблоном. Например, для лямбда-выражения
auto f = [ ] ( auto х) { return normal i ze ( х ) ; } ;

оператор вызова функции класса замыкания имеет следующий вид:
class SomeCompi lerGeneratedClassName {
puЬlic :
template
/ / См . возвращаемый тип auto
auto operator ( ) ( Т х) const / / в разделе 1 . 3
{ return normal i ze ( x ) ; }
1 1 Прочая функциональность
11 класса замыкания
};

В этом примере единственное, что делает лямбда-выражение с параметром х, - это пере­
дает его функции norma l i ze. Если пorma l i ze рассматривает значения lvalue не так, как
значения rvalue, это лямбда-выражение написано некорректно, поскольку оно всегда
передает функции norma l i ze lvalнe (параметр х ) , даже если переданный в лямбда-выра­
жение аргумент представляет собой rvalue.
234

Глава б. Лямбда-выражения

Корректным способом написания лямбда-выражения является прямая передача х
в norma l i ze . Это требует внесения в код двух изменений. Во-первых, х должен быть уни­
версальной ссылкой (см. раздел 5.2), а во-вторых, он должен передаваться в norma l i z e
с использованием std : : forward (см. раздел 5.3). Концептуально это требует тривиаль­
ных изменений:
auto f
[ ] (auto&& х )
{ return norma l i ze (std: : forward (x) ) ; 1 :
=

Однако между концепцией и реализацией стоит вопрос о том, какой тип передавать
в std : : forward, т.е. вопрос определения того, что должно находиться там, где я написал
" ? ? ?':

Обычно, применяя прямую передачу, вы находитесь в шаблонной функции, прини­
мающей параметр типа т, так что вам надо просто написать s t d : : forward. В обоб­
щенном лямбда-выражении такой параметр типа т вам недоступен. Имеется т в шабло­
низированном операторе operator ( ) в классе замыкания, сгенерированном лямбда-вы­
ражением, но сослаться на него из лямбда-выражения невозможно, так что это никак не
помогает.
В разделе 5.6. поясняется, что если lvalue-apryмeнт передается параметру, являюще­
муся универсальной ссылкой, то типом этого параметра становится lvalue-ccылкa. Если
же передается rvalue, параметр становится rvаluе-ссылкой. Это означает, что вне лямбда­
выражения мы можем определить, является ли переданный аргумент lvalue или rvalue,
рассматривая тип параметра х. Ключевое слово decl t ype дает нам возможность сде­
лать это (см. раздел 1 .3). Если было передано lvalue, dec l t ype ( х ) даст тип, являющийся
lvаluе-ссылкой. Если же было передано rvalue, decl t уре ( х ) даст тип, являющийся rvаluе­
ссылкой.
В разделе 5.6 поясняется также, что при вызове std : : fo rward соглашения требуют,
чтобы для указания lvalue аргументом типа была lvalue-ccылкa, а для указания rvalue тип, не являющийся ссылкой. В нашем лямбда-выражении, если х привязан к lvalue,
declt уре ( х ) даст lvalue-ccылкy. Это отвечает соглашению. Однако, если х привязан
к rvalue, d e c l t уре ( х ) вместо типа, не являющегося ссылкой, даст rvalue-ccылкy. Но
взглянем на пример реализации std : : forward в С++ 14 из раздела 5.6:
/ / В пространстве
template
Т&& forward ( remove_reference_t& param) // имен std
return static_cast ( param) ;

Если клиентский код хочет выполнить прямую передачу rvalue типа Widget, он обыч­
но инстанцирует st d : : forward типом W idget (т.е. типом, не являющимся ссылочным),
и шаблон std : : forward дает следующую функцию:
Widget& & forward (Widget& param)
{

б.3.

11 Инстанцирование для
1 1 std: : forward, когда

Испопьзуйте п араметры decltype дл я auto&& для передачи с помощью std::forward

235

return static_ca st (param) ; // Т является Widget

Но рассмотрим, что произойдет, если код клиента намерен выполнить прямую пере­
дачу того же самого rvalue типа Widget, но вместо следования соглашению об определе­
нии Т как не ссылочного типа определит его как rvа\uе-ссылку, т.е. рассмотрим, что слу­
чится, если Т определен как W idge t & & . После начального инстанцирования std : : forwa rd
и применения std : : remove_re fe rence_ t, но до свертывания ссылок (еще раз обратитесь
к разделу 5.6) std : : forward будет выглядеть следующим образом:
Widqet&& & & forward (Widqet& param)
{
return static_cast ( param) ;
/ / (до

11 Инстанцирование
11 std : : forward nри
// Т, равном Widget & &
сворачивания ссьuюк)

Применение правила сворачивания ссылок о том, что rvа\uе-ссылка на rvа\uе-ссылку ста­
новится одинарной rvа\uе-ссылкой, приводит к следующему инстанцированию:
Widge t & & forward (Widget& param)
{
return static_cast (param) ;
1 1 ( после

11 Инстанцирование
11 std : : forward при
/ / Т , равном Widget&&
сворачивания ссьmок )

Если вы сравните это инстанцирование с инстанцированием, получающимся в резуль­
тате вызова s t d : : forward с т, равным Widget, то вы увидите, что они идентичны. Это оз­
начает, что инстанцирование s t d : : forward типом, представляющим собой rvа\uе-ссылку,
дает тот же результат, что и инстанцирование типом, не являющимся ссылочным.
Это чудесная новость, так как dec l t ype ( х ) дает rvа\uе-ссылку, когда в качестве ар­
гумента для параметра х нашего лямбда-выражения передается rvalue. Выше мы уста­
новили, что, когда в наше лямбда-выражение передается lvalue, dec lt уре ( х ) дает со­
ответствующи й соглашению тип для передачи в s t d : : forwa rd, и теперь мы понимаем,
что для rvalue dec l t ype ( х ) дает тип для передачи st d : : fo rwa rd, который не соответ­
ствует соглашению, но тем не менее приводит к тому же результату, что и тип, соот­
ветствующий соглашению. Так что как для lvalue, так и для rvalue передача declt ype ( х )
в s t d : : forward дает н ам желаемый результат. Следовательно, наше лямбда-выражение
с прямой передачей может быть записано следующим образом:
auto f
[ ] (auto&& х )
{ return norma l i ze (std : : forward ( x ) ) ; ) ;
=

Чтобы это лямбда-выражение принимало любое количество параметров, нам, по сути,
надо только шесть точек, поскольку лямбда-выражения в С++ 14 могут быть с перемен­
ным ч ислом аргументов:
auto f
[ ] ( auto&& . . . x s )
{ return normal i ze ( std : : forward ( xs ) . . . ) ; ) ;
=

236

Глава б. Лямбда-выражения

Сл едует запомнить


Используйте для параметров а u t о & & при их прямой передаче с помощью
s t d : : forward ключевое слово decl t ype.

6.4. Предпочитайте nямбда-выражения
применению s td : : Ьind
std : : Ьi пd в C++ l l является преемником s td : : Ьiпdlst и s t d : : Ьiпd2пd из С++98,
но неформально этот шаблон является частью стандартной библиотеки еще с 2005 года.
Именно тогда Комитет по стандартизации принял документ, известный как TRl, который
включал спецификацию Ь i пd. (В TRl Ь i пd находился в отдельном пространстве имен,
так что обращаться к нему надо было как к s t d : : t r 1 : : Ьi пd, а не к s t d : : Ьi пd, а кроме
того, некоторые детали его интерфейса были иными.) Эта история означает, что неко­
торые программисты имеют за плечами десятилетний опыт использования s t d : : Ь i пd.
Если вы один из них, вы можете не быть склонными отказываться от столь долго верой
и правдой служившего вам инструмента. Это можно понять, и все же в данном случае
лучше его сменить, поскольку лямбда-выражения С++ 1 1 почти всегда являются лучшим
выбором, чем std : : Ь iпd. Что касается С++ 14, то здесь лямбда-выражения являются на­
стоящим кладом.
В этом разделе предполагается, что вы хорошо знакомы с std : : Ьiпd. Если это не так,
вы, вероятно, захотите получить базовые знания о нем, прежде чем продолжить чтение.
Что ж, это похвально, тем более что никогда не знаешь, не встретишься ли с применени­
ем std : : Ьiпd в коде, который придется читать или поддерживать.
Как и в разделе 6.2, я называю функциональные объекты, возвращаемые std : : Ьi пd,

Ьiпd-объектами.
Наиболее важная причина, по которой следует предпочитать лямбда-выражения, за­
ключается в их большей удобочитаемости. Например, предположим, что у нас есть функ­
ция для настройки будильника:
11 Тип для момента времени ( см . синтаксис в разделе 3 . 3 )

usiпg Time

std: : chroпo : : steady_clock : : time_point ;

// См . "enum class" в разделе 3 . 4
eпum class Sound { Веер, Sireп, Whistle } ;
// Тип для продолжительности промежутка времени

usiпg Duratioп

=

std: : chroпo : : steady_clock: : duration;

11 В момент t издать звук s продолжительностью d
void setAlarm (Time t , Sound s , Duration d) ;

Далее предположим, что в некоторой точке программы мы определили, что хотим, что­
бы будильник был отключен в течение часа, после чего подал сигнал продолжительностью
6.4. Предпочитайте лямбда-выражения применению std::Ьind

237

30 с. Сам звук остается неопределенным. Мы можем написать лямбда-выражение, которое
изменяет интерфейс setAl ann так, что необходимо указать только звук:
11 setSoundL ( " 1 " означает "лямбда-выражение " ) - функциональный
11 объект, позволяющий указать сигнал будильника , который должен
11 звучать через час в течение 30 с

auto setSoundL
[ ] ( Sound s )

=

{
11 Делает доступными компоненты std : : chrono

using namespace std : : chrono;
setAlarш (steady clock : : now ( ) +hours (l ) , / / Будильник через
_
11 час, звучит
s,

11 30 секунд

seconds (ЗO) ) ;
f;

Я выделил вызов setAlann в лямбда-выражении. Он выглядит, как обычный вызов функ­
ции, и даже читатель с малым опытом в лямбда-выражениях может понять, что переданный
лямбда-выражению параметр s передается в качестве аргумента функции setAlarrn.
В С++ 1 4 этот код можно упростить, используя стандартные суффиксы для секунд (s),
миллисекунд (ms), часов (h) и друтих единиц, основанных на поддержке пользователь­
ских литералов в С++ 14. Эти суффиксы определены в пространстве имен std : : l iterals,
так что приведенный выше код переписывается как

auto setSoundL
[ ] ( Sound s )

=

{

using namespace std : : chrono;
11 Суффиксы С++ 1 4

using namespace std: : literals ;

setAlann ( steady_clock : : now ( ) +
s,

lh, / / С++ 1 4 , смысл

11 тот же , что
// И ВЬП!Jе

ЗОs ) ;
1;

Наша первая попытка написать соответствующий вызов std : : Ьind приведена ниже.
Она содержит ошибки, которые мы вскоре исправим, но главное - что правильный код
более сложен, и даже эта упрощенная версия приводит к ряду важных вопросов:
using namespace std: : chrono;
using namespace std : : litera l s ;
using namespace std : : placeholders ;
auto setSoundВ =
std : : Ьind ( setAlarm,
steady_clock : : now ( ) + lh,
1,

30s) ;
238

Глава 6. Лямбда-выражения

11 Как и ранее
11 Необходимо дпя " 1 "
1 1 "В" означает "bind"
//

Оимбка ! См .

ниже

Я хотел бы выделить вызов setAlarm, как делал это в лямбда-выражении, но здесь
нет вызова, который можно было бы выделить. Читатели этого кода просто должны
знать, что вызов set SoundB приводит к вызову setAlarm со временем и продолжитель­
ностью, указанными в вызове s t d : : Ьi nd. Для непосвященных заполнитель " l" выгля­
дит магически, но даже знающие читатели должны в уме отобразить число в заполнителе
на позицию в списке параметров s t d : : Ьind, чтобы понять, что первый аргумент вызо­
ва setSoundB передается в setAlarm в качестве второго аргумента. Тип этого аргумента
в вызове std : : Ьind не определен, так что читатели должны проконсультироваться с объ­
явлением setAlarm, чтобы выяснить, какой аргумент передается в set SoundB.
Но, как я уже говорил, этот код не совсем верен. В лямбда-выражении очевидно,
что выражение " s t e ady_c l o c k : : now ( ) + l h" представляет собой аргумент s e t A l a rm.
Оно будет вычислено при вызове s e t A l a rm. Это имеет смысл: мы хотим, чтобы бу­
дильник заработал через час после вызова set A l arm. Однако в вызове std : : Ь i nd вы­
ражение " st eady_clock : : now ( ) +lh" передается в качестве аргумента в std : : Ьi nd, а не
в setAlarm. Это означает, что выражение будет вычислено при вызове std : : Ьind и полу­
ченное время будет храниться в сгенерированном Ьind-объекте. В результате будильник
сработает через час после вызова s t d : : b i nd, а не через час после вызова setAlarm!
Решение данной проблемы требует указания для st d : : Ь i nd отложить вычисление
выражения до вызова setAlarm, и сделать это можно с помощью вложения еще двух вы­
зовов s td : : Ьind в первый:
auto setSoundВ
std: : bind ( setAlarm,
=

std : : bind (std: : plus O ,
std: :Ьind (steady_clock : : now) ,
lh) '

1,
ЗО s ) ;

Если вы знакомы с шаблоном s t d : : p l u s из С++98, вас может удивить то, что
между угловыми скобками не указан тип, т.е. что код содержит " st d : : p l u s '', а не
"st d : : plus < t yp e > ". В C++ l4 аргумент типа шаблона для шаблонов стандартных опера­
торов в общем случае может быть опущен, так что у нас нет необходимости указывать
его здесь. С++ l l такой возможности не предоставляет, так что в С++ l l эквивалентный
лямбда-выражению std : : Ьind имеет следующий вид:
using namespace std : : chrono ;
/ / Как и ранее
using namespace s t d : : placeholde r s ;
auto setSoundB
std : : bind ( setAlarm,
std : : Ьind ( std: : plus< steady clock : : time_JIOint> ( )
std: : Ьind ( steady_clock : : now ) ,
hours ( l ) ) ,
=

_

,

6.4. Предпочитайте л ямбда-выражения применению std::Ьiпd

239

1,

seconds ( 30 ) ) ;
Если в этот момент лямбда-выражение не выглядит для вас гораздо более привлека­
тельным, вам, пожалуй, надо сходить к окулисту.
При перегрузке setAlarm возникают новые вопросы. Предположим, что перегрузка
получает четвертый параметр, определяющий громкость звука:
enum class Volume { Nonnal , Loud, LoudPlusPlus } ;
void setAlarm (Time t, Sound s , Duration d, Volume v) ;

Лямбда-выражение продолжает работать, как и ранее, поскольку разрешение пере­
грузки выбирает трехарrументную версию setAlarm:
auto setSoundL = / / Как и ранее
[ ] ( Sound s )
{

using narnespace std : : chrono;
setAlann ( steady_clock : : now ( ) + lh, // ОК, вызывает
/ / setAlarm с тремя
s,
/ / аргументами
ЗОs ) ;
};

Вызов же std : : Ьi nd теперь не компилируется:
/ / Ошибка ! Какая из
auto setSoundВ =
std : : bind ( setAlarm,
/ / функций setAlarm?
std : : Ьind ( std : : plus ( ) ,
std: : Ьind ( steady_clock : : now) ,
lh) ,
1,

ЗОs ) ;
Проблема в том, что у компиляторов нет способа определить, какая из двух функций
setAl a rm должна быть передана в s t d : : Ьi nd. Все, что у них есть, - это имя функции,
а одно имя приводит к неоднозначности.
Чтобы вызов s t d : : Ьind комп илировался, setAlarm должна быть приведена к кор­
ректному типу указателя на функцию:
using SetAlarmЗParam'l'ype

=

void (*) (Ti.me t, Sound s, Duration d) ;

11 Теперь все в порядке
auto setSoundВ =
s td : : Ьind ( зtatic_cast ( setAlarm) ,
std: : Ьind ( std: : plus ( ) ,
std : : Ьiпd ( steady_clock : : now) ,
lh) '
1,

ЗОs ) ;

240

Гла ва 6. Лямбда-выражения

Но это привносит еще одно различие между лямбда-выражениями и s t d : : Ь i nd. Вну­
три оператора вызова функции для s e t SoundL (т.е. оператора вызова функции класса за­
мыкания лямбда-выражения) вызов s e t A l a rm представляет собой обычный вызов функ­
ции, который может быть встроен компиляторами:
setSoundL ( Sound : : Si ren ) ; // Тело setAlarm может быть встроено

Однако вызов s td : : Ь i n d получает указатель на функцию s e tA l a rm, а это означает,
что внутри оператора вызова функции s e tSoundB (т.е. оператора вызова функции Ьind­
объекта) имеет место вызов setAl a rm с помощью указателя на функцию. Компиляторы
менее склонны к встраиванию вызовов функций, выполняемых через указатели, а это
означает, что вызовы s e tAl a rm посредством s e t SoundB будут встроены с куда меньшей
вероятностью, чем вызовы посредством s e tSoundL:
setSoundВ ( Sound : : Si ren ) ; // Тело setAlarm вряд ли будет встроено

Таким образом, вполне возможно, что применение лямбда-выражений приводит к ге­
нерации более быстрого кода, чем применение s t d : : Ьi nd.
Пример setAlarm включает только простой вызов функции. Если вы хотите сделать
что-то более сложное, то перевес в пользу лямбда-выражений только увеличится. Рас­
смотрим, например, такое лямбда-выражение С++ 1 4, которое выясняет, находится ли
аргумент между минимальным ( lowVa l ) и максимальным (highVa l ) значениями, где
l owVa l и highVal являются локальными переменными.
auto betweenL
[ lowVa l , highVa l ]
( const auto& val )
{ return lowVal = minLen) &&
(newName . length ( ) , 263

R

auto_ptr, 1 26

в

rvalue, 1 6

s

Ьind, 232; 237

с

shared_ptr, 1 3 3

т

const, 1 06
constexpr, 1 05

thread(), 245

const_iterator, 95

thread_local, 2 5 1

D
decltype, 23; 36; 235

true_type, 1 93
typedef, 73
typename, 75

Е
enaЫe_if, 1 9 5

u
un ique_ptr, 1 26

enum, 7 8

using, 74

explicit, 298

F
false_type, 1 93

v
vector

bool, 5 5

final, 92
forward(), 1 69; 204
function, 5 1

ошибка 11изайна,

68

volatile, 272; 277

w
initializer_ list, 34; 63

weak_ptr, 1 42

is_base_of, 1 98
is_constructiЬle, 2 0 1

Большая тройка,

L
Вывод типа, 53

lvalнe, 1 6

м
make_shared(), 1 46
make_uniqнe(), 1 4 6
move(), 1 66
mutaЬle, 1 1 2

Б
1 19

в

auto, 3 1
decltype, 38
шаблона, 23

г
Гарантия безопасности исключения
базовая, 1 8
строгая, 1 8

д
Диспетчеризация дескрипторов, 1 9 1

3
Завершающий возвращаемый тип, 37
Зависимый тип, 75
Замыкание, 1 9; 22 1
класс, 222
и
Идиома
Pimpl, 1 32; 1 55
RAII, 257
захват ресурса есть иню1иализация, 257
указателя на реализацию, 1 32; 1 55
явной типизации инициализатора, 58
Инициализация
копированием, 299
прямая, 299
унифицированная, 62
фигурная, 62; 2 1 3
и vector, 67
Исключение
и деструктор, 1 03
спецификация, 98
Исключительное владение, 1 27
Итератор, 95
к
Класс
замыкания, 222
перечислений, 78
шаблонный, 1 9
Конструктор
перемещающий, 1 1 7
Контекстное ключевое слово, 92
Контракт, 1 03
л
Лямбда-выражение, 22 1
инициализирующий захват, 229
и прямая передача, 235
обобщенное, 234
обобщенный захват, 231
режим захвата, 222
м
Массив, 28
Метапроrраммирование, 76; 1 94; 200

302

П редметный укаэатель

н
Неопределенное поведение, 20; 4 1 ; 258; 274
и предусловие, 1 04
о
Объект
вызываемый, 1 8
со статическим временем хранения, 228
функциональный, 1 8
Объявление, 1 9
псевдонима, 74
Определение, 1 9
Оптимизация
возвращаемого значения, 1 8 1
избыточных чтений и записей, 276
малых строк, 2 1 О; 289
п
Параллельные вычисления, 245
ложное пробуждение, 266
локальная память потока, 251
общее состояние, 261
переменная усло11ия, 265
поток, 246
Перекрытие, 88
Перемещение, 1 1 7
ограничения, 2 1 1
почленное, 1 1 7; 2 1 0
Перечисление, 78
базовый тип, 79
с об11астью видимости, 78
Превышение подписки, 247
Преобразование типа
неявное сужающее, 63
Прокси-класс, 56
и auto, 57
Прямая передача, 1 8; 1 65; 2 1 2; 235
р
Размещение, 293

с
Свертывание ссылок. 1 75; 202; 203
Совместное владение, 1 33
Срезка, 290
Ссылка
передаваемая, 1 72
универса11ь11ая, 1 7 1 ; 190
и перегрузка, 1 84

ограничения, 1 94
Ссылочн ы й квалификатор, 89; 92
Счетчик ссылок, 1 33
т
Тип
базовый перечисления, 79
заоисимый, 75
литера11ьный, 1 08
llСПОЛНЫЙ, 1 56

соойстоа, 76
то11ько перемещаемый, 1 27; 165
у
Указатель
интс1111ектуа11ьный, 20
на функцию, 30
обычный, 20
Унифицированная и нициализация, 62
ф
Функциональный объект. См. Объект
функциональный
Функция
make, 1 47

аргумент, 1 8
параметр, 1 8; 1 66
lvalue, 1 69
перекрытие, 88
сигнатура, 1 9
удаленная, 85
ч11ен, специальная, 1 1 6
шаблонная, 1 9
Фьючерс, 245
ш
Шаблон
вариативный, 2 1 3
выражения, 57
класса, 19
отключенный, 1 95
проектирования
наблюдатель, 145
прокси, 57
странно повторяющийся шаблон, 1 39
функции, 1 9

Предметны й указатель

303

ПРОГРАММИРОВАНИЕ.
ПРИНЦИПЫ И ПРАКТИКА С ИСПОЛЬЗОВАНИЕМ С++
ВТОРОЕ ИЗДАНИЕ
Бьярне Страуструп

Эта к н и га - учебн и к по
програ м м и рован и ю. Несмотря
на то что его автор - создател ь
язы ка

С++,

к н и га не

посвящена этому язы ку; он
и грает в бол ьшей степен и
илл юстрати в н у ю рол ь. К н и га
задумана как ввод н ы й курс
по п рогра м м и рова н и ю с
п р и мера м и програм м н ы х
ре ше н и й н а язы ке

С+ +

и оп исы вает ш и рок и й
круг пон яти й и п риемов
п рогра м м и рова н и я ,
необход и м ы х для того, чтобы
стать п рофессионал ьн ы м
п рогра м м истом.

В

первую очередь к н и га

адресована нач и нающ и м

www.williamspuЫishing.com

п рогра м м истам, н о о н а будет
полезна и профессионалам,
которые на йдут в ней м ного
новой и нформац и и , а
главное, смогут узнать точ ку
зрен и я создател я язы ка
на совре ме н н ые метод ы
п рогра м м и рова н и я .

ISBN

978-5-8459- 1 949-б

в

продаже

С++