С. Мейерс - Эффективный и современный C++ (1114942), страница 20
Текст из файла (страница 20)
Ссылочные квалификаторы функции-члена являются одной из менее известных возможностейС++ 1 1 , так что не удивляйтесь, если вы до сих пор ничего о них не слышали. Онипозволяют ограничить использование функции-члена только объектами lvalue илитолько объектами rvalue.
Для использования этих квалификаторов функции-члены не обязаны быть виртуальными.class WidgetpuЫic :void doWork ( ) & ;Эта версияесли *thisЭта версияесли * thisdoWork применима , толькопредставляет собой lvaluedoWork применима , толькопредставляет собой rvalue};11111111Widget makeWidget ( ) ;11 Фабричная функция ( возвращает rvalue )void doWork ( ) &&;З.б.
Объявляйте перекрывающие функции как override89Widget w ;11 Обычный объект ( lvalue )111111rnakeWidget ( ) . doWork ( ) ;11w . 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 BasepuЬ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 voidvirtual voidvirtual voidvirtual 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 .
Объяв ляйте перекрывающие функции как override91Обратите внимание, что в этом примере часть работы состояла в том, чтобы объявить mf 4в классе Base как виртуальную функцию. Большинство ошибок, связанных с перекрытием,совершаются в производном классе, но можно сделать такую ошибку и в базовом классе.Стратегия использования ключевого слова override во всех перекрытиях производного класса способна на большее, чем просто позволить компиляторам сообщать, когда функции, которые должны быть перекрытиями, ничего не перекрывают. Они также могут помочь вам оценить последствия предполагаемого изменения сигнатуры виртуальной функции в базовом классе. Если производные классы везде используют override, вы можетепросто изменить сигнатуру и перекомпилировать систему.
Вы увидите, какие повреждениянанесли своей системе (т.е. сколько классов перестали компилироваться), и после этогосможете принять решение, стоит ли изменение сигнатуры таких хлопот. Без override выдолжны были бы надеяться на наличие достаточно всеобъемлющих тестов, поскольку, какмы видели, виртуальные функции, которые предназначены перекрывать функции базовогокласса, но не делают этого, не приводят ни к какой диагностике со стороны компилятора.В С++ всегда имелись ключевые слова, но С++ 1 1 вводит два контекстных ключевыхслова (contextual keywords)override и f i nal'. Эти ключевые слова являются зарезервированными, но только в некоторых контекстах. В случае override оно имеет зарезервированное значение только тогда, когда находится в конце объявления функции-члена. Этозначит, что если у вас есть старый код, который уже использовал имя override, его не надоизменять при переходе к С++ 1 1 :-c lass WarningpuЬlic :11 Потенциально старый класс из С++98void 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 DataTypestd : : vector<douЫe>; / / См . информацию о11 using в разделе 3 . 3DataType& data ( ) { return value s ; }private :DataType values ;};Вряд ли это наиболее инкапсулированный дизайн, который видел свет, но оставимэтот вопрос в стороне и рассмотрим, что происходит в следующем клиентском коде:Widget w;auto valsl=w .
data ( ) ;/ / Копирует w . values в valslВозвращаемый тип W i dget : : data представляет собой \vа\uе-ссылку (чтобы быть точным - std: : vector<douЬle>&), а поскольку \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. Объяв л яйте перекрывающие функции как override93для оптимизации на основе правила "как если бы"4, но было бы глупо полагаться та то, чтоваш компилятор найдет способ им воспользоваться.)Нам необходим способ указать, что, когда data вызывается для Widget, являющегося rvalue, результат также будет представлять собой rvalue. Использование ссылочныхквалификаторов для перегрузки data для Widget, являющихся lvalue и rvalue, делает этовозможным:class WidgetpuЫic :using DataTypestd : : vector<douЫe>;DataType& data ( ) &{ return values ; }DataType&& data ( ) &&{ return std: : move (values ) ;/ / Для lvalue11 возвращает11 Для rvalue11 возвращаетWidge t ,lvalueWidge t ,rvalueprivate :DataType value s ;};Обратите внимание на разные возвращаемые типы перегрузок data.