Язык программирования C++.

19.4. Указатель на член класса.

Указатель на член класса (pointer to member) — это указатель, способный указывать на нестатический член класса. Обычно указатель указывает на объект, но указатель на член класса идентифицирует только член класса объекта, а не весь объект. Статические члены класса не являются частью конкретного объекта, поэтому для указания на них не нужен никакой специальный синтаксис. Указатели на статические члены являются обычными указателями.

Тип указателя на член класса объединяет тип класса и тип члена этого класса. Такие указатели инициализируют как указывающие на определенный член класса, не указывая объект, которому принадлежит этот член. При применении указателя на член класса предоставляется объект, член класса которого предстоит использовать.

Для демонстрации работы указателей на члены класса воспользуемся упрощенной версией класса Screen из раздела 7.3.1:

class Screen {

public:

 typedef std::string::size_type pos;

 char get_cursor() const { return contents[cursor]; }

 char get() const;

 char get(pos ht, pos wd) const;

private:

 std::string contents;

 pos cursor;

 pos height, width;

};

19.4.1. Указатели на переменные-члены.

Подобно любым указателям, при объявлении указателя на член класса используется символ *, означающий, что объявляемое имя является указателем. В отличие от обычных указателей, указатель на член класса включает также имя класса, содержащего этот член. Следовательно, символу * должна предшествовать часть имяКласса::, означающая, что определяемый указатель способен указывать на член класса имяКласса. Например:

// pdata может указывать на член типа string константного (или не

// константного) объекта класса Screen

const string Screen::*pdata;

Приведенный выше код объявляет pdata "указателем на член класса Screen, обладающий типом const string". Переменные-члены константного объекта сами являются константами. Объявление указателя pdata как указателя на тип const string позволяет использовать его для указания на член любого объекта класса Screen, константного или нет. Взамен указатель pdata применим только для чтения, но не для записи в член класса, на который он указывает.

При инициализации (или присвоении) указателя на член класса следует заявить, на который член он указывает. Например, можно заставить указать pdata указывать на переменную-член contents неопределенного объекта класса Screen следующим образом:

pdata = &Screen::contents;

Здесь оператор обращения к адресу применяется не к объекту в памяти, а к члену класса Screen.

Конечно, по новому стандарту проще объявить указатель на член класса при помощи ключевых слов auto или decltype:

auto pdata = &Screen::contents;

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

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

Подобно операторам доступа к членам (member access operator), . и ->, существуют два оператора доступа к указателю на член класса, .* и - >*, позволяющие предоставить объект и обращаться к значению указателя для доступа к члену этого объекта:

Screen myScreen, *pScreen = &myScreen;

// .* обращение к значению pdata для доступа к содержимому члена данного

// объекта класса myScreen

auto s = myScreen.*pdata;

// ->* обращение к значению pdata для доступа к содержимому члена

// объекта, на который указывает pScreen

s = pScreen->*pdata;

Концептуально эти операторы выполняют два действия: обращаются к значению указателя на член класса, чтобы получить доступ к необходимому члену; затем, подобно операторам обращения к членам, они обращаются к члену данного объекта непосредственно ( .*) или через указатель ( ->*).

Функция, возвращающая указатель на переменную-член.

К указателям на члены применимы обычные средства управления доступом. Например, член contents класса Screen является закрытым. В результате указатель pdata выше должен использоваться в члене класса Screen, его дружественном классе, либо произойдет ошибка.

Поскольку переменные-члены обычно являются закрытыми, как правило, нельзя получать указатель на саму переменную-член. Вместо этого, если такой класс, как Screen, желает предоставить доступ к своему члену contents, то он определил бы функцию, возвращающую указатель на эту переменную-член:

class Screen {

public:

 // data() - статический член, возвращающий указатель на член класса

 static const std::string Screen::*data()

  { return &Screen::contents; }

 // другие члены, как прежде

};

Здесь в класс Screen добавлена статическая функция-член, возвращающая указатель на переменную-член contents класса Screen. Тип возвращаемого значения этой функции совпадает с типом первоначального указателя pdata. Читая тип возвращаемого значения справа налево, можно заметить, что функция data() возвращает указатель на член класса Screen, имеющий тип string и являющийся константой. Тело функции применяет оператор обращения к адресу к переменной-члену contents. Таким образом, функция возвращает указатель на переменную-член contents класса Screen.

Когда происходит вызов функции data(), возвращается указатель на член класса:

// data() возвращает указатель на член contents класса Screen

const string Screen::*pdata = Screen::data();

Как и прежде, указатель pdata указывает на член класса Screen, но не на фактические данные. Чтобы использовать указатель pdata, следует связать его с объектом типа Screen:

// получить содержимое объекта myScreen

auto s = myScreen.*pdata;

Упражнения раздела 19.4.1.

Упражнение 19.11. В чем разница между обычным указателем на данные и указателем на переменную-член?

Упражнение 19.12. Определите указатель на член класса, способный указывать на член cursor класса Screen. Получите через этот указатель значение Screen::cursor.

Упражнение 19.13. Определите тип, способный представить указатель на член bookNo класса Sales_data.

19.4.2. Указатели на функции-члены.

Вполне можно также определить указатель, способный указывать на функцию-член класса. Подобно указателям на переменные-члены, самый простой способ создания указателя на функцию-член — это использовать ключевое слово auto для автоматического выведения типа:

// указатель pmf способен указывать на функцию-член класса Screen,

// возвращающую тип char и не получающую никаких аргументов

auto pmf = &Screen::get_cursor;

Как и указатель на переменную-член, указатель на функцию-член объявляется с использованием синтаксиса имяКласса::*. Подобно любому другому указателю на функцию (см. раздел 6.7), указатель на функцию-член определяет тип возвращаемого значения и список типов параметров функции, на которую может указывать этот указатель. Если функция-член является константной (см. раздел 7.1.2) или ссылочной (см. раздел 13.6.3), следует также добавить квалификатор const или квалификатор ссылки.

Подобно обычным указателям на функцию, если функция-член перегружена, следует явно указать, какая именно функция имеется в виду (см. раздел 6.7). Например, указатель на версию функции get() с двумя параметрами можно объявить так:

char (Screen::*pmf2)(Screen::pos, Screen::pos) const;

pmf2 = &Screen::get;

Круглые скобки вокруг части Screen::* в этом объявлении необходимы из-за приоритета. Без круглых скобок компилятор воспримет следующий код как (недопустимое) объявление функции:

// ошибка: у функции, не являющейся членом класса p, не может быть

// спецификатора const

char Screen::*p(Screen::pos, Screen::pos) const;

Это объявление пытается определить обычную функцию по имени p, которая возвращает указатель на член класса Screen типа char. Поскольку объявляется обычная функция, за объявлением не может быть спецификатора const.

В отличие от обычных указателей на функцию, нет никакого автоматического преобразования между функцией-членом и указателем на этот член:

// pmf указывает на член класса Screen, не получающий аргументов и

// возвращающий тип char

pmf = &Screen::get; // нужно явно использовать оператор обращения к

                    // адресу

pmf = Screen::get;  // ошибка: нет преобразования в указатель для

                    // функций-членов

Использование указателя на функцию-член.

Как и при использовании указателя на переменную-член, для вызова функции-члена через указатель на член класса используются операторы .* и - >*:

Screen myScreen, *pScreen = &myScreen;

// вызов функции, на которую указывает указатель pmf объекта,

// на который указывает указатель pScreen

char c1 = (pScreen->*pmf)();

// передает аргументы 0, 0 версии функции get() с двумя параметрами

// объекта myScreen

char c2 = (myScreen.*pmf2)(0, 0);

Вызовы (myScreen->*pmf)() и (pScreen.*pmf2)(0,0) требуют круглых скобок, поскольку приоритет оператора вызова выше, чем приоритет оператора указателя на член класса.

Без круглых скобок вызов myScreen.*pmf() был бы интерпретирован как myScreen.*(pmf()).

Этот код требует вызвать функцию pmf() и использовать ее возвращаемое значение как операнд оператора указателя на член класса ( .*). Но pmf — не функция, поэтому данный код ошибочен.

Язык программирования C++.Из-за разницы приоритетов операторов вызова объявления указателей на функции-члены и вызовы через такие указатели должны использовать круглые скобки: (С::*p)(parms) и (obj.*p) (args).

Использование псевдонимов типов для указателей на члены.

Псевдонимы типа или typedef (см. раздел 2.5.1) существенно облегчают чтение указателей на члены. Например, следующий код определяет псевдоним типа Action как альтернативное имя для типа версии функции get() с двумя параметрами:

// Action - тип, способный указывать на функцию-член класса Screen,

// возвращающую тип char и получающую два аргумента типа pos

using Action =

 char (Screen::*)(Screen::pos, Screen::pos) const;

Action — это другое имя для типа "указатель на константную функцию-член класса Screen, получающую два параметра типа pos и возвращающую тип char". Используя этот псевдоним, можно упростить определение указателя на функцию get() следующим образом:

Action get = &Screen::get; // get указывает на член get() класса Screen

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

// action() получает ссылку на класс Screen и указатель на его

// функцию-член

Screen& action(Screen&, Action = &Screen::get);

Функция action() получает два параметра, которые являются ссылками на объект класса Screen, и указатель на функцию-член класса Screen, получающую два параметра типа pos и возвращающую тип char. Функцию action() можно вызвать, передав ей указатель или адрес соответствующей функции-члена класса Screen:

Screen myScreen;

// эквивалентные вызовы:

action(myScreen);      // использует аргумент по умолчанию

action(myScreen, get); // использует предварительно определенную

                       // переменную get

action(myScreen, &Screen::get); // передает адрес явно

Язык программирования C++.Псевдонимы типа облегчают чтение и написание кода, использующего указатели.

Таблицы указателей на функцию-член.

Как правило, перед использованием указатели на функции и указатели на функции-члены хранят в таблице функций (см. раздел 14.8.3). Когда у класса есть несколько членов того же типа, такая таблица применяется для выбора одного из набора этих членов. Предположим, что класс Screen дополнен несколькими функциями-членами, каждая из которых перемещает курсор в определенном направлении:

class Screen {

public:

 // другие члены интерфейса и реализации, как прежде

 Screen& home(); // функции перемещения курсора

 Screen& forward();

 Screen& back();

 Screen& up();

 Screen& down();

};

Каждая из этих новых функций не получает никаких параметров и возвращает ссылку на вызвавший ее объект класса Screen.

Можно определить функцию move(), способную вызвать любую из этих функций и выполнить указанное действие. Для поддержки этой новой функции в класс Screen добавлен статический член, являющийся массивом указателей на функции перемещения курсора:

class Screen {

public:

 // другие члены интерфейса и реализации, как прежде

 // Action - указатель, который может быть присвоен любой из

 // функций-членов перемещения курсора

 using Action = Screen&(Screen::*)();

 // задать направление перемещения;

 // перечисления описаны в разделе 19.3

 enum Directions { HOME, FORWARD, BACK, UP, DOWN };

 Screen& move(Directions);

private:

 static Action Menu[]; // таблица функций

};

Массив Menu содержит указатели на каждую из функций перемещения курсора. Эти функции будут храниться со смещениями, соответствующими перечислителям перечисления Directions. Функция move() получает перечислитель и вызывает соответствующую функцию:

Screen& Screen::move(Directions cm) {

 // запустить элемент по индексу cm для объекта this

 return (this->*Menu[cm])(); // Menu[cm] указывает на функцию-член

}

Вызов move() обрабатывается следующим образом: выбирается элемент массива Menu по индексу cm. Этот элемент является указателем на функцию-член класса Screen. Происходит вызов функции-члена, на которую указывает этот элемент от имени объекта, на который указывает указатель this.

Когда происходит вызов функции move(), ему передается перечислитель, указывающий направление перемещения курсора:

Screen myScreen;

myScreen.move(Screen::HOME); // вызывает myScreen.home

myScreen.move(Screen::DOWN); // вызывает myScreen.down

Остается только определить и инициализировать саму таблицу:

Screen::Action Screen::Menu[] = { &Screen::home,

                                  &Screen::forward,

                                  &Screen::back,

                                  &Screen::up,

                                  &Screen::down,

};

Упражнения раздела 19.4.2.

Упражнение 19.14. Корректен ли следующий код? Если да, то что он делает? Если нет, то почему?

auto pmf = &Screen::get_cursor; pmf = &Screen::get;

Упражнение 19.15. В чем разница между обычным указателем на функцию и указателем на функцию-член?

Упражнение 19.16. Напишите псевдоним типа, являющийся синонимом для указателя, способного указать на переменную-член avgprice класса Sales_data.

Упражнение 19.17. Определите псевдоним типа для каждого отдельного типа функции-члена класса Screen.

19.4.3. Использование функций-членов как вызываемых объектов.

Как уже упоминалось, для вызова через указатель на функцию-член, нужно использовать операторы .* и ->* для связи указателя с определенным объектом. В результате, в отличие от обычных указателей на функцию, указатель на функцию-член класса не является вызываемым объектом; эти указатели не поддерживают оператор вызова функции (см. раздел 10.3.2).

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

auto fp = &string::empty; // fp указывает на функцию empty()

                          // класса string

// ошибка: для вызова через указатель на член класса следует

// использовать оператор .* или ->*

find_if(svec.begin(), svec.end(), fp);

Алгоритм find_if() ожидает вызываемый объект, но предоставляется указатель на функцию-член fp. Этот вызов не будет откомпилирован, поскольку код в алгоритме find_if() выполняет примерно такой оператор:

// проверяет применимость данного предиката к текущему элементу,

// возвращает true

if (fp(*it)) // ошибка: для вызова через указатель на член класса

             // следует использовать оператор ->*

Использование шаблона function для создания вызываемого объекта.

Один из способов получения вызываемого объекта из указателя на функцию-член подразумевает использование библиотечного шаблона function (см. раздел 14.8.3):

function<bool (const string&)> fcn = &string::empty;

find_if(svec.begin(), svec.end(), fcn);

Здесь шаблону function указано, что empty() — это функция, которая может быть вызвана со строкой и возвращает значение типа bool. Обычно объект, для которого выполняется функция-член, передается неявному параметру this. Когда шаблон function используется при создании вызываемого объекта для функции-члена, следует преобразовать код так, чтобы сделать этот неявный параметр явным.

Когда объект шаблона function содержит указатель на функцию-член, класс function знает, что для вызова следует использовать соответствующий оператор указателя на член класса. Таким образом, можно предположить, что у функции find_if() будет код наподобие следующего:

// если it является итератором в функции find_if(), то *it - объект

// в заданном диапазоне

if (fcn(*it)) // fcn - имя вызываемого объекта в функции find_if()

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

// если it является итератором в функции find_if(), то *it - объект

// в заданном диапазоне

if (((*it).*p)()) // p - указатель на функцию-член в функции fcn

При определении объекта шаблона function следует указать тип функции, сигнатура которой определяет представляемые вызываемые объекты. Когда вызываемой объект является функцией-членом, первый параметр сигнатуры должен представить (обычно неявный) объект, для которого будет выполнена функция-член. Передаваемая шаблону function сигнатура должна определять, будет ли объект передан как указатель или как ссылка.

При определении вызываемого объекта fcn() было известно, что нужно вызвать функцию find_if() для последовательности строковых объектов. Следовательно, от шаблона function требовалось создать вызываемый объект, получающий объекты класса string. Если бы вектор содержал указатели на тип string, от шаблона function требовалось бы ожидать указатель:

vector<string*> pvec;

function<bool (const string*)> fp = &string::empty;

// fp получает указатель на string и использует оператор ->* для вызова

// функции empty()

find_if(pvec.begin(), pvec.end(), fp);

Использование шаблона mem_fn для создания вызываемого объекта.

Язык программирования C++.Чтобы использовать шаблон function, следует предоставить сигнатуру вызова члена, который предстоит вызвать. Но можно позволить компилятору вывести тип функции-члена при использовании другого библиотечного средства, шаблона mem_fn, определенного, как и шаблон function, в заголовке functional. Как и шаблон function, шаблон mem_fn создает вызываемый объект из указателя на член класса. В отличие от шаблона function, шаблон mem_fn выведет тип вызываемого объекта из типа указателя на член класса:

find_if(svec.begin(), svec.end(), mem_fn(&string::empty));

Здесь шаблон mem_fn(&string::empty) создает вызываемый объект, получающий строковый аргумент и возвращающий логическое значение.

Вызываемый объект, созданный шаблоном mem_fn, может быть вызван для объекта или указателя:

auto f = mem_fn(&string::empty); // f получает string или string*

f(*svec.begin()); // ok: передача объекта string; f использует .* для

                  // вызова empty()

f(&svec[0]);      // ok: передача указателя на string; f использует .->

                  // для вызова empty()

Фактически шаблон mem_fn можно считать как будто создающим вызываемый объект с перегруженным оператором вызова функции — один получает тип string*, а другой — string&.

Использование функции bind() для создания вызываемого объекта.

Для создания вызываемого объекта из функции-члена можно также использовать функцию bind() (см. раздел 10.3.4):

// связать каждую строку из диапазона

// с неявным первым аргументом empty()

auto it = find_if(svec.begin(), svec.end(),

           bind(&string::empty, _1));

Подобно шаблону function, при использовании функции bind() следует сделать явным обычно неявный параметр функции-члена, представляющий объект, с которым будет работать функция-член. Подобно шаблону mem_fn, первый аргумент вызываемого объекта, создаваемого функцией bind(), может быть либо указателем, либо ссылкой на тип string:

auto f = bind(&string::empty, _1);

f(*svec.begin()); // ok: аргумент - строка f, использует .* для вызова

                  // функции empty()

f(&svec[0]); // ok: аргумент - указатель на строку f использует .->

             // для вызова функции empty()

Упражнения раздела 19.4.3.

Упражнение 19.18. Напишите функцию, использующую алгоритм count_if() для подсчета количества пустых строк в заданном векторе.

Упражнение 19.19. Напишите функцию, получающую вектор vector<Sales_data> и находящую первый элемент, средняя цена которого превосходит заданное значение.