С. Мейерс - Эффективный и современный C++ (1114942), страница 52
Текст из файла (страница 52)
Цель этого действия заключается в том, чтобы однафункция (которой передаются параметры) получила в точности те же объекты, которыепереданы другой функции (которая выполняет передачу). Тем самым исключается передача параметров по значению, поскольку при этом выполняется копирование исходнопереданных объектов; мы же хотим, чтобы передающая функция была способна работать с изначально переданными объектами. Указатели также исключаются, поскольку мыне хотим заставлять вызывающий код передавать указатели. Когда речь идет о передачеобщего назначения, мы работаем с параметрами, представляющими собой ссылки.Прямая передача означает, что мы передаем не просто объекты, но и их основныехарактеристики: их типы, являются они lvalue или rvalue, объявлены они как const илиvo la t i le.
В сочетании с наблюдением о том, что мы будем иметь дело со ссылочнымипараметрами, это означает, что мы будем использовать универсальные ссылки (см. раздел 5.2), поскольку только параметры, являющиеся универсальными ссылками, хранят информацию о том, какие аргументы - являющиеся lvalue или rvalue - были им переданы.Предположим, что у нас есть некоторая функция f и мы хотели бы написать функцию(по правде говоря, шаблон функции), которая выполняет передачу ей.
Ядро того, что намнадо, имеет следующий вид:template<typename Т>void fwd ( T&& param){11 Принимает любой аргументf ( std : : forward<Т> (param) ) ; / / Передача аргументавfПередающие функции по своей природе являются обобщенными. Шаблон fwd, например, принимает аргумент любого типа, и он передает все, что бы ни получил. Логическим продолжением этой обобщенности являются передающие функции, являющиесяне просто шаблонами, а шаблонами с произвольным количеством аргументов (вариативными шаблонами (variadic templates)).
Вариативная разновидность fwd выглядит следующим образом:template<typename . . . Ts>void fwd ( Ts&& . . . params )11 Принимает любые аргументы{f ( std : : forward<Ts> ( 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<int>& v ) ;В этом случае вызов f с инициализаторами в фигурных скобках компилируется:1 1 ОК, " { 1 , 2 , 3 } " неявно преобразуется11 в s t d : : vector<int>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<int>.При косвенном вызове 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 ilfwd ( i l ) ;={ 1,2,3 } ; // Тип il выводится как11 s td : : initiali zer l ist<int>// ОК, прямая передача 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<int> widgetDa t a ;widgetData . reserve (Widget : : МinVals ) ; / / Использование MinVa ls214Гпава 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 по ссылке в общем случае требует, чтобыони были определены, и это требование может привести к неудачному применению прямой передачи там, где эквивалентный код без прямой передачи будет успешен.Возможно, вы обратили внимание на некоторые юркие слова, употребленные мноювыше. Я сказал, что код "не должен" компоноваться.