Главная страница
Навигация по странице:

  • Упражнение 14.31. В классе StrBlobPtr не определен конструктор копий, оператор присвоения и деструктор. Почему

  • Упражнение 14.33. Сколько операндов может иметь перегруженный оператор вызова функции

  • Язык программирования C Пятое издание


    Скачать 1.85 Mb.
    НазваниеЯзык программирования C Пятое издание
    Дата15.07.2019
    Размер1.85 Mb.
    Формат файлаpdf
    Имя файла620354-www.libfox.ru.pdf
    ТипДокументы
    #84130
    страница39 из 54
    1   ...   35   36   37   38   39   40   41   42   ...   54

    Упражнение 14.29. Почему не были определены константные версии операторов инкремента и декремента?
    14.7. Операторы доступа к членам
    Операторы обращения к значению (*) и стрелка (->) обычно используются в классах,
    представляющих итераторы, и в классах интеллектуального указателя (см. раздел 12.1).
    Вполне логично добавить эти операторы в класс StrBlobPtr: class StrBlobPtr { public: std::string& operator*() const { auto p = check(curr, "dereference past end"); return (*p)[curr]; //
    (*p) - вектор, на который указывает этот
    // объект
    Page 711/1103

    } std::string* operator->() const {
    // передать реальную работу оператору обращения к значению return &this->operator*();
    }
    // другие члены как прежде
    };
    Оператор обращения к значению проверяет принадлежность curr диапазону, и если это так,
    то возвращает ссылку на элемент, обозначенный значением curr. Оператор стрелки не делает ничего сам, он вызывает оператор обращения к значению и возвращает адрес возвращенного им элемента.
    Оператор стрелка (arrow) должен быть определен как функция-член класса.
    Оператор обращения к значению (dereference) необязательно должен быть членом класса,
    но, как правило, его тоже определяют как функцию-член.
    Следует заметить, что эти операторы определены как константные члены. В отличие от операторов инкремента и декремента, выборка элемента никак не изменяет состояния объекта класса StrBlobPtr. Обратите также внимание на то, что эти операторы возвращают ссылку или указатель на неконстантную строку. Причина этого в том, что объект класса
    StrBlobPtr, как известно, может быть связан только с неконстантным объектом класса StrBlob
    (см. раздел 12.1.6).
    Эти операторы можно использовать таким же способом, которым используются соответствующие операторы с указателями и итераторами вектора:
    StrBlob a1 = {"hi", "bye", "now"};
    StrBlobPtr p(a1); // p указывает на вектор в a1
    *p = "okay"; // присвоить первый элемент a1 cout << p->size() << endl; // выводит 4, размер первого элемента в a1 cout << (*p).size() << endl; // эквивалент p->size() Ограничения на возвращаемое значение оператора стрелки
    Подобно большинству других операторов (хотя это и плохая идея), оператор operator* можно определить как выполняющий некие действия по своему усмотрению. Таким образом,
    Page 712/1103
    оператор operator* можно определить как возвращающий, например, фиксированное значение, скажем, 42, или выводящий содержимое объекта, к которому он применен, или что то еще. Но для перегруженного оператора стрелки это не так. Оператор стрелки никогда не изменяет своего фундаментального назначения: доступа к члену класса. При перегрузке оператора стрелки можно изменить объект, из которого стрелка выбирает определенный член, но нельзя изменить тот факт, что она выбирает член класса.
    В коде point->mem часть point должна быть указателем на объект класса или объектом класса с перегруженным оператором operator->. В зависимости от типа части point код point->mem может быть эквивалентен следующему:
    (*point).mem; // point - указатель встроенного типа point.operator()->mem; // point - объект типа класса
    В противном случае код ошибочен. Таким образом, код point->mem выполняется следующим образом.
    1. Если point — указатель, то применение встроенного оператора стрелки означает эквивалент выражения (*point).mem. Указатель обращается к значению члена класса и выбирает его из объекта. Если у типа, на объект которого указывает point, нет члена по имени mem, то этот код ошибочен.
    2. Если point — объект класса, в котором определен оператор operator->, то результат вызова point.operator->() используется для выбора члена mem. Если результат является указателем, то для него выполняется этап 1. Если результат является объектом, класс которого сам обладает перегруженным оператором operator->(), то с этим объектом повторяется данный этап. Процесс продолжается до тех пор, пока не будет возвращен указатель на объект с означенным членом или некое другое значение, означающее ошибочность кода.
    Перегруженный оператор стрелки должен возвращать либо указатель на тип класса, либо объект типа класса, определяющего собственный оператор стрелки.Упражнения раздела 14.7
    Упражнение 14.30. Добавьте операторы обращения к значению и стрелки в класс StrBlobPtr и класс ConstStrBlobPtr из упражнения 12.22 раздела 12.1.6. Обратите внимание, что операторы класса ConstStrBlobPtr должны возвращать константные ссылки, поскольку переменная-член data класса ConstStrBlobPtr указывает на константный вектор.

    Упражнение 14.31. В классе StrBlobPtr не определен конструктор копий, оператор присвоения и деструктор. Почему?
    Упражнение 14.32. Определите класс, содержащий указатель на класс StrBlobPtr. Определите перегруженный оператор стрелки для этого класса.
    14.8. Оператор вызова функции
    Page 713/1103

    Классы, перегружающие оператор вызова, позволяют использовать объекты этого типа как функции. Поскольку объекты таких классов способны хранить состояние, они могут оказаться существенно гибче обычных функций.
    В качестве простого примера рассмотрим структуру absInt, обладающую оператором вызова,
    возвращающим абсолютное значение своего аргумента: struct absInt { int operator()(int val) const { return val < 0 ? -val : val;
    }
    };
    Этот класс определяет одну функцию: оператор вызова функции. Этот оператор получает аргумент типа int и возвращает абсолютное значение аргумента.
    Оператор вызова используется применительно к списку аргументов объекта класса absInt способом, который выглядит как вызов функции: int i = -42; absInt absObj; // объект класса с оператором вызова функции int ui = absObj(i); // передача i в absObj.operator()
    Хотя absObj — это объект, а не функция, его вполне можно вызвать. При вызове объект выполняет свой перегруженный оператор вызова. В данном случае этот оператор получает значение типа int и возвращает его абсолютное значение.
    Оператор вызова функции должен быть функцией-членом. Класс может определить несколько версий оператора вызова, каждая из которых должна отличаться количеством или типом параметров.
    Объект класса, определяющего оператор вызова, называется объектом функции (function object). Такие объекты действуют как функции, поскольку их можно вызвать.Классы объектов функций с состоянием
    У класса объекта функции, как у любого другого класса, могут быть и другие члены, кроме оператора operator(). Классы объекта функции зачастую содержат переменные-члены,
    используемые для настройки действий в операторе вызова.
    В качестве примера определим класс, выводящий строковый аргумент. По умолчанию класс будет писать в поток cout и выводить пробел после каждой строки. Позволим также пользователям класса предоставлять другой поток для записи и другой разделитель. Этот класс можно определить следующим образом: class PrintString { public:
    Page 714/1103

    PrintString(ostream &o = cout, char c = ' '): os(o), sep(c) { } void operator()(const string &s) const { os << s << sep; } private: ostream &os; // поток для записи char sep; // символ завершения после каждого вывода
    };
    У класса есть конструктор, получающий ссылку на поток вывода, и символ, используемый как разделитель. Как аргументы по умолчанию (см. раздел 6.5.1) для этих параметров используется поток cout и пробел. Тело оператора вызова функции использует эти члены при выводе данной строки.
    При определении объектов класса PrintString можно использовать аргументы по умолчанию или предоставлять собственные значения для разделителя или потока вывода:
    PrintString printer; // использует аргументы по умолчанию; вывод в cout printer(s); // выводит s и пробел в cout
    PrintString errors(cerr, '\n'); errors(s); // выводит s и новую строку в cerr
    Объекты функции обычно используют как аргументы для обобщенных алгоритмов. Например,
    для вывода содержимого контейнера можно использовать класс PrintString и библиотечный алгоритм for_each() (см. раздел 10.3.2): for_each(vs.begin(), vs.end(), PrintString(cerr, '\n'));
    Третий аргумент алгоритма for_each() является временным объектом типа PrintString,
    инициализируемый потоком cerr и символом новой строки. Вызов функции for_each() выводит каждый элемент vs в поток cerr, разделяя их новой строкой. Упражнения раздела 14.8

    Упражнение 14.33. Сколько операндов может иметь перегруженный оператор вызова функции?
    Упражнение 14.34. Определите класс объекта функции для выполнения действий условного оператора: оператор вызова этого класса должен получать три параметра. Он должен проверить свой первый параметр и, если эта проверка успешна, возвратить свой второй параметр; в противном случае он должен возвратить свой третий параметр.
    Page 715/1103

    Упражнение 14.35. Напишите класс, подобный классу PrintString, который читает строку из потока istream и возвращает строку, представляющую прочитанное. При неудаче чтения следует возвратить пустую строку.
    Упражнение 14.36. Используя класс из предыдущего упражнения, организуйте чтение со стандартного устройства ввода, сохраняя каждую строку в векторе как элемент.
    Упражнение 14.37. Напишите класс, проверяющий равенство двух значений. Используйте этот объект и библиотечные алгоритмы для написания кода замены всех экземпляров заданного значения в последовательности.
    14.8.1. Лямбда-выражения — объекты функции
    В предыдущем разделе объект PrintString использовался как аргумент в вызове функции for_each(). Это похоже на программу, написанную в разделе 10.3.2, где использовалось лямбда-выражение. Написанное лямбда-выражение компилятор преобразовывает в безымянный объект безымянного класса (см. раздел 10.3.3). Классы, созданные из лямбда-выражения, содержат перегруженный оператор вызова функции. Рассмотрим,
    например, лямбда-выражение, передававшееся как последний аргумент функции stable_sort():
    // сортировать слова по размеру, поддерживая алфавитный порядок среди
    // слов того же размера stable_sort(words.begin(), words.end(),
    [](const string &a, const string &b)
    { return a.size() < b.size();});
    Это действует как безымянный объект класса, который выглядел бы примерно так: class ShorterString { public: bool operator()(const string &s1, const string &s2) const
    { return s1.size() < s2.size(); }
    };
    У этого класса есть один член, являющийся оператором вызова функции, получающим две строки и сравнивающий их длины. Список параметров и тело функции те же, что и у лямбда-выражения. Как уже упоминалось в разделе 10.3.3, по умолчанию лямбда-выражения не могут изменять свои захваченные переменные. В результате по умолчанию оператор вызова функции в классе, созданном из лямбда-выражения, является константной функцией-членом. Если лямбда-выражение объявляется как mutable, то оператор вызова не будет константным.
    Page 716/1103

    Вызов функции stable_sort() можно переписать так, чтобы использовать этот класс вместо лямбда-выражения: stable_sort(words.begin(), words.end(), ShorterString());
    Третий аргумент — недавно созданный составной объект класса ShorterString. Код в функции stable_sort() будет вызывать этот объект каждый раз, когда он сравнивает две строки. При вызове объекта будет выполнено тело его оператора вызова, возвращающего значение true,
    если размер первой строки будет меньше, чем второй. Классы, представляющие лямбда-выражения с захваченными переменными
    Как уже упоминалось, при захвате лямбда-выражением переменной по ссылке разработчик должен сам гарантировать существование переменной, на которую ссылается ссылка, во время выполнения лямбда-выражения (см. раздел 10.3.3). Поэтому компилятору разрешено использовать ссылку непосредственно, не сохраняя ее как переменную-член в созданном классе.
    Переменные, которые захватываются по значению, напротив, копируются в лямбда-выражение (см. раздел 10.3.3). В результате классы, созданные из лямбда-выражений, переменные которых захватываются по значению, имеют переменные-члены, соответствующие каждой такой переменной. У этих классов есть также конструктор для инициализации этих переменных-членов значениями захваченных переменных. В примере раздела 10.3.2 лямбда-выражение использовалось для поиска первой строки, длина которой была больше или равна заданному значению:
    // получить итератор на первый элемент, размер которого >= sz auto wc = find_if(words.begin(), words.end(),
    [sz](const string &a)
    Созданный класс выглядел бы примерно так: class SizeComp {
    SizeComp(size_t n) : sz(n) {} // параметр для каждой захваченной
    // переменной
    // оператор вызова с тем же типом возвращаемого значения, параметрами
    // и телом, как у лямбда-выражения bool operator()(const string &s) const
    { return s.size() >= sz; } private:
    Page 717/1103
    size_t sz; // переменная-член для каждой переменной, захваченной
    // по значению
    };
    В отличие от класса ShorterString, у этого класса есть переменная-член и конструктор для ее инициализации. У этого синтезируемого класса нет стандартного конструктора; чтобы использовать этот класс, следует передать аргумент:
    // получить итератор на первый элемент, размер которого >= sz auto wc = find_if(words.begin(), words.end(), SizeComp(sz));
    У классов, созданных из лямбда-выражения, есть удаленный стандартный конструктор,
    удаленные операторы присвоения и стандартный деструктор. Будет ли у класса стандартный или удаленный конструктор копий/перемещения, зависит обычно от способа и типа захватываемых переменных-членов (см. раздел 13.1.6 и раздел 13.6.2). Упражнения раздела
    14.8.1
    Упражнение 14.38. Напишите класс, проверяющий соответствие длины заданной строки указанному значению. Используйте такой объект в программе для оповещения о количестве слов во входном файле, имеющих размеры от 1 до 10 включительно.
    Упражнение 14.39. Перепишите предыдущую программу так, чтобы сообщать количество слов размером от 1 до 9 и 10 или более.
    Упражнение 14.40. Перепишите функцию biggies() из раздела 10.3.2 так, чтобы использовать объект функции вместо лямбда-выражения.
    Упражнение 14.41. Как по-вашему, существенно ли добавление лямбда-выражений по новому стандарту? Объясните, когда имеет смысл использовать лямбда-выражение, а когда класс вместо него.
    14.8.2. Библиотечные объекты функций
    Стандартная библиотека определяет набор классов, представляющих арифметические,
    реляционные и логические операторы. Каждый класс определяет оператор вызова, который применяет одноименный оператор. Например, у класса plus есть оператор вызова функции,
    который применяет оператор + к паре операндов; класс modulus определяет оператор вызова, применяющий бинарный оператор %; класс equal_to применяет оператор ==; и т.д.
    Эти классы являются шаблонами, которым передается один тип. Он определяет тип параметра оператора вызова. Например, класс plus<string> применяет строковый оператор суммы к объектам класса string; у класса plus<int> типом операндов будет int;
    класс plus<Sales_data> применяет оператор + к объектам класса Sales_data; и т.д.: plus<int> intAdd; //
    Page 718/1103
    объект функции, способный сложить
    // два значения типа int negate<int> intNegate; // объект функции, способный изменить знак
    // значения типа int
    // использование оператора intAdd::operator(int, int) для
    // сложения чисел 10 и 20 int sum = intAdd(10, 20); // эквивалент sum = 30 sum = intNegate(intAdd(10, 20)); // эквивалент sum = 30
    // использование оператора intNegate::operator(int) для создания
    // числа -10 как второго параметра выражения intAdd::operator(int, int) sum = intAdd(10, intNegate(10)); // sum = 0
    Эти типы, перечислены в табл. 4.2, определены в заголовке functional.
    Таблица 14.2. Библиотечные объекты функций Арифметические Реляционные Логические plus<Type> equal_to<Type> logical_and<Type> minus<Type>
    not_equal_to<Type> logical_or<Type> multiplies<Type> greater<Type>
    logical_not<Type> divides<Type> greater_equal<Type> modulus<Type>
    less<Type> negate<Type> less_equal<Type> Применение библиотечного объекта функции с алгоритмами
    Классы объектов функций, представляющие операторы, зачастую используются для переопределения заданного по умолчанию оператора, используемого алгоритмом. Как уже упоминалось, по умолчанию алгоритмы сортировки используют оператор operator< для сортировки последовательности в порядке возрастания. Для сортировки в порядке убывания можно передать объект типа greater. Этот класс создает оператор вызова, который вызывает оператор "больше" основного типа элемента. Предположим, например, что svec — это вектор
    Page 719/1103
    типа vector<string>:
    // передает временный объект функции, который применяет
    // оператор > к двум строкам sort(svec.begin(), svec.end(), greater<string>());
    Это сортирует вектор в порядке убывания. Третий аргумент — безымянный объект типа greater<string>. Когда функция sort() сравнит элементы, вместо оператора < типа элемента она применит переданный объект функции greater. Этот объект применит оператор
    > к элементам типа string.
    Одним из важнейших аспектов этих библиотечных объектов функций является то, что библиотека гарантирует их работоспособность с указателями. Помните, что результат сравнения двух несвязанных указателей непредсказуем (см. раздел 3.5.3). Но может понадобиться сортировать вектор указателей на основании их адреса в памяти. Хотя сделать это самостоятельно непросто, вполне можно применить один из библиотечных объектов функции: vector<string *> nameTable; // вектор указателей
    // ошибка: указатели в nameTable не связаны, результат < непредсказуем sort(nameTable.begin(), nameTable.end(),
    [](string *a, string *b) { return a < b; });
    // ok: библиотека гарантирует, что less для типов указателя определен sort(nameTable.begin(), nameTable.end(), less<string*>());
    Стоит также обратить внимание на то, что ассоциативные контейнеры используют для упорядочивания своих элементов объект типа less<key_type>. В результате можно определить набор (set) указателей или использовать указатель как ключ в карте (map) без необходимости определять тип less самостоятельно. Упражнения раздела 14.8.2
    Упражнение 14.42. Используя библиотечные объекты и адаптеры функций, определите объекты для:
    (a) Подсчета количеств значений больше 1024
    (b) Поиска первой строки, не равной pooh
    (c) Умножения всех значений на 2
    Упражнение 14.43. Используя библиотечные объекты функций, определите, делимо ли переданное значение типа int на некий элемент в контейнере целых чисел.
    Page 720/1103

    14.8.3. Вызываемые объекты и тип function
    В языке С++ есть несколько видов вызываемых объектов: функции и указатели на функции,
    лямбда-выражения (см. раздел 10.3.2), объекты, созданные функцией bind() (см. раздел
    10.3.4), и классы с перегруженным оператором вызова функции.
    Подобно любому другому объекту, у вызываемого объекта есть тип. Например, у каждого лямбда-выражения есть собственный уникальный (безымянный) тип класса. Типы функций и указателей на функции зависят от типа возвращаемого значения, типа аргумента и т.д.
    Однако два вызываемых объекта с разными типами могут иметь ту же сигнатуру вызова (call signature). Сигнатура вызова определяет тип возвращаемого значения вызываемого объекта и тип (типы) аргумента, которые следует передать при вызове.
    Сигнатура вызова соответствует типу функции. Например: int(int, int)
    Функция этого типа получает два числа типа int и возвращает значение типа int. Разные типы могут иметь одинаковую сигнатуру вызова
    Иногда необходимо использовать несколько вызываемых объектов с одинаковой сигнатурой вызова, как будто это тот же тип. Рассмотрим, например, следующие разные типы вызываемых объектов:
    // обычная функция int add(int i, int j) { return i + j; }
    // лямбда-выражение, создающее безымянный класс объекта функции auto mod = [](int i, int j) { return i % j; };
    // класс объекта функции struct div { int operator()(int denominator, int divisor) { return denominator / divisor;
    }
    };
    Каждый из этих вызываемых объектов применяет арифметическую операцию к своим параметрам. Даже при том, что у каждого из них разный тип, сигнатура вызова у них одинакова:
    Page 721/1103
    int(int, int)
    Эти вызываемые объекты можно использовать для написания простого калькулятора. Для этого следует определить таблицу функций (function table), хранящую "указатели" на вызываемые объекты. Когда программе понадобится выполнить некую операцию, она просмотрит таблицу и найдет соответствующую функцию.
    В языке С++ таблицы функций довольно просто реализовать при помощи карт (map). В
    данном случае как ключ используем строку, соответствующую символу оператора; значение будет функцией, реализующей этот оператор. При необходимости выполнить заданный оператор индексируется карта и осуществляется вызов возвращенного элемента. Если бы все эти функции были автономными и необходимо было использовать только парные операторы для типа int, то карту можно было бы определить так:
    // сопоставляет оператор с указателем на функцию, получающую два целых
    // числа и возвращающую целое число map<string, int(*)(int, int)> binops;
    Указатель add можно поместить в карту binops следующим образом:
    // ok: add - указатель на функцию соответствующего типа binops.insert({"+", add}); // {"+", add} - пара раздел 11.2.3
    Но сохранить в карте binops объекты mod или div не получится: binops.insert({"%", mod}); // ошибка: mod - не указатель на функцию
    Проблема в том, что mod — это лямбда-выражение, и у каждого лямбда-выражения есть собственный тип класса. Этот тип не соответствует типу значений, хранимых в карте binops.
    Библиотечный тип function
    Эту проблему можно решить при помощи нового библиотечного типа function, определенного в заголовке functional; возможные операции с типом function приведены в табл. 14.3.
    Таблица 14.3. Операции с типом function function<T> f; f — пустой объект класса function,
    способный хранить вызываемые объекты с сигнатурой вызова, эквивалентной типу функции T
    (т.е. Т — это retType(args)) function<T> f(nullptr); Явное создание пустого объекта класса function function<T> f(obj); Сохранение копии вызываемого объекта obj в объекте f f Когда f используется как условие; оно истинно, если содержит вызываемый объект, и ложно в противном случае f( args ) Вызывает объект f с передачей аргументов args Типы, определенные как члены шаблона function<T> result_type Тип возвращаемого значения объекта функции этого типа argument_type first_argument_type
    Page 722/1103
    second_argument_type Типы, определяемые, когда у типа T есть один или два аргумента.
    Если у типа T есть один аргумент, то argument_type — синоним его типа. Если у типа T два аргумента, то first_argument_type и second_argument_type — синонимы их типов
    Тип function — это шаблон. Подобно другим шаблонам, при создании его экземпляра следует указать дополнительную информацию. В данном случае этой информацией является сигнатура вызова объекта, который сможет представлять данный конкретный тип function. Как и у других шаблонов, этот тип определяют в угловых скобках: function<int(int, int)>
    Здесь был объявлен тип function, способный представлять вызываемые объекты,
    возвращающие целочисленный результат и имеющие два параметра типа int. Этот тип можно использовать для представления любого из типов приложения калькулятора: function<int(int, int)> f1 = add; // указатель на функцию function<int(int, int)> f2 = div(); // объект класса объекта функции function<int(int, int)> f3 = [](int i, int j) // лямбда-выражение
    { return i * j; }; cout << f1(4,2) << endl; // выводит 6 cout << f2(4,2) << endl; // выводит 2 cout << f3(4,2) << endl; // выводит 8
    Теперь карту можно переопределить, используя тип function:
    // таблица вызываемых объектов,
    // соответствующих всем бинарным операторам
    // все вызываемые объекты должны получать по два int и возвращать int
    // элемент может быть указателем на функцию, объектом функции или
    //
    Page 723/1103
    лямбда-выражением map<string, function<int(int, int)>> binops;
    В эту карту можно добавить каждый из вызываемых объектов приложения, будь то указатель на функцию, лямбда-выражение или объект функции: map<string, function<int(int, int)>> binops = {
    {"+", add}, // указатель на функцию
    {"-", std::minus<int>()}, // объект библиотечной функции
    {"/", div()}, // пользовательский объект функции
    {"*", [](int i, int j) { return i * j; }}, // безымянное
    // лямбда-выражение
    {"%", mod} }; // именованный объект лямбда-выражения
    В карте пять элементов. Хотя все лежащие в основе вызываемые объекты имеют различные типы, каждый из них можно хранить в общем типе function<int(int, int)>.
    Как обычно, при индексировании карты возвращается ссылка на ассоциированное значение.
    При индексировании карты binops возвращается ссылка на объект типа function. Тип function перегружает оператор вызова. Этот оператор вызова получает собственные аргументы и передает их хранимому вызываемому объекту: binops["+"](10, 5); // вызов add(10, 5) binops["-"](10, 5); // использует оператор; вызов объекта minus<int> binops["/"](10, 5); // использует оператор; вызов объекта div binops["*"](10, 5); // вызов объекта лямбда-функции binops["%"](10, 5); // вызов объекта лямбда-функции
    Page 724/1103

    Здесь происходит вызов каждой из операций, хранимых в карте binops. В первом вызове возвращаемый элемент является указателем на функцию, указывающим на функцию add().
    Вызов binops["+"](10, 5) использует этот указатель для вызова функции add с передачей ей значений 10 и 5. Следующий вызов, binops["-"], возвращает объект класса function, хранящий объект типа std::minus<int>. Затем можно вызвать оператор этого объекта и других.
    Перегруженные функции и тип function
    Нельзя непосредственно хранить имя перегруженной функции в объекте типа function: int add(int i, int j) { return i + j; }
    Sales_data add(const Sales_data&, const Sales_data&); map<string, function<int(int, int)>> binops; binops.insert({"+", add}); // ошибка: какой именно add?
    Один из способов разрешения двусмысленности подразумевает хранение указателя на функцию (см. раздел 6.7) вместо имени функции: int (*fp)(int, int) = add; // указатель на версию add,
    // получающую два int binops.insert({"+", fp}); // ok: fp указывает на правую версию add
    В качестве альтернативы для устранения неоднозначности можно использовать лямбда-выражение:
    // ok: использование лямбда-выражения
    // для устранения неоднозначности при
    // выборе используемой версии add binops.insert({"+", [](int a, int b) {return add(a, b);} });
    Вызов в теле лямбда-выражения передает два целых числа. Этому вызову может соответствовать только та версия функции add(), которая получает два целых числа, а следовательно, эта функция и применяется при выполнении лямбда-выражения.
    Класс function в новой библиотеке никак не связан с классами unary_function и binary_function,
    которые были частью прежних версий библиотеки. Эти классы были заменены более общей функцией bind() (см. раздел 10.3.4). Упражнения раздела 14.8.3
    Page 725/1103

    Упражнение 14.44. Напишите собственную версию простого калькулятора, способного выполнять бинарные операции.
    14.9. Перегрузка, преобразование и операторы
    В разделе 7.5.4 упоминалось, что неявный конструктор, который может быть вызван с одним аргументом, определяет неявное преобразование. Такие конструкторы преобразовывают объект типа аргумента в тип класса. Можно также определить преобразование из типа класса. Для этого нужно определить оператор преобразования. Конструкторы преобразования и операторы преобразования определяют преобразования типа класса (class-type conversion). Такие преобразования называются также пользовательскими преобразованиями (user-defined conversion).
    14.9.1. Операторы преобразования
    Оператор преобразования (conversion operator) — это специальный вид функции-члена класса. Общий синтаксис функции преобразования имеет следующий вид: operator тип () const; где тип — это имя типа. Операторы преобразования могут быть определены для любого типа
    (кроме void), который может быть типом возвращаемого значения функции (см. раздел 6.1).
    Преобразование в тип массива или функции недопустимо. Однако преобразование в тип указателя на данные или функцию, а также ссылочные типы вполне возможны.
    У операторов преобразования нет явно заданного типа возвращаемого значения и нет параметров, их следует определять как функции-члены. Операции преобразования обычно не должны изменять преобразуемый объект. В результате операторы преобразования обычно определяют как константные члены.
    Функция преобразования должна быть функцией-членом, у нее не определен тип возвращаемого значения и пустой список параметров. Функция обычно должна быть константой. Определение класса с оператором преобразования
    Для примера определим небольшой класс, представляющий целое число в диапазоне от 0 до
    255: class SmallInt { public:
    SmallInt(int i = 0): val(i) { if (i < 0 || i > 255)
    Page 726/1103
    throw std::out_of_range("Bad SmallInt value");
    } operator int() const { return val; } private: std::size_t val;
    };
    Класс SmallInt определяет преобразования в и из своего типа. Конструктор преобразует значения арифметического типа в тип SmallInt. Оператор преобразования преобразует объекты класса SmallInt в тип int:
    SmallInt si; si = 4; // неявно преобразует 4 в SmallInt, а затем
    // вызывает SmallInt::operator= si + 3; // неявно преобразует si в int с последующим целочисленным
    // суммированием
    Хотя компилятор применяет только одно пользовательское преобразование за раз (см.
    раздел 4.11.2), неявное пользовательское преобразование можно предварить или сопроводить стандартным (встроенным) преобразованием (см. раздел 4.11.1). В результате конструктору SmallInt можно передать любой арифметический тип. Точно так же можно использовать оператор преобразования для преобразования объекта класса SmallInt в int, а затем преобразовать полученное значение типа int в другой арифметический тип:
    // аргумент типа double преобразуется в int с использованием
    // встроенного преобразования
    SmallInt si = 3.14; // вызов конструктора SmallInt(int)
    // оператор преобразования класса SmallInt преобразует si в int si + 3.14; //
    Page 727/1103
    int преобразуется в double с использованием встроенного
    // преобразования
    Поскольку операторы преобразования применяются неявно, нет никакого способа передать аргументы этим функциям. Следовательно, операторы преобразования не могут быть определены как получающие параметры. Хотя функция преобразования не определяет тип возвращаемого значения, каждая из них должна возвратить значение соответствующего типа:
    class SmallInt; operator int(SmallInt&); // ошибка: не член класса class SmallInt { public: int operator int() const; // ошибка: тип возвращаемого значения operator int(int = 0) const; // ошибка: список параметров operator int*() const { return 42; } // ошибка: 42 не указатель
    }; Внимание! Не злоупотребляйте функциями преобразования
    Как и в случае с перегруженными операторами, разумное использование функций преобразования помогает существенно упростить работу разработчика класса и сделать полученный класс удобным в применении. Однако здесь есть две потенциальные ловушки:
    определение слишком большого количества функций преобразования может привести к неоднозначности кода, а некоторые преобразования могут оказаться скорее вредными, чем полезными.
    Для примера рассмотрим класс Date, представляющий данные о дате. Вполне очевидно, что имеет смысл предоставить способ преобразования объекта класса Date в объект типа int. Но какое значение должна возвращать функция преобразования? Она могла бы возвратить десятичное представление года, месяца и дня. Например, 30 июля 1989 года могло бы быть представлено как значение 19800730 типа int. В качестве альтернативы оператор преобразования мог бы возвращать целое число, соответствующее количеству дней, начиная с некоторой эпохальной даты. Счетчик мог бы считать дни с 1 января 1970 года или некой другой отправной точки. У обоих преобразований есть желаемое свойство, что более поздние даты соответствуют большим целым числам, что может быть очень полезно.
    Проблема в том, что нет единого и полного соответствия между объектом типа Date и значением типа int. В таких случаях лучше не определять оператор преобразования. Вместо него класс должен определить один или несколько обычных членов, чтобы извлекать эту информацию в различных форматах. Операторы преобразования могут привести к
    Page 728/1103
    удивительным результатам
    На практике классы редко предоставляют операторы преобразования. Пользователи,
    вероятней всего, будут просто удивлены, случись преобразование автоматически, без помощи явного преобразования. Но из этого эмпирического правила есть одно важное исключение: преобразование в тип bool является вполне общепринятым для классов.
    По прежним версиям стандарта перед классами с преобразованием в тип bool стояла проблема: поскольку тип bool арифметический, объект этого типа, допускающего преобразование в тип bool, применим в любом контексте, где ожидается арифметический тип.
    Такие преобразования могут происходить весьма удивительными способами. В частности,
    если бы у класса istream было преобразование в тип bool, то следующий код вполне компилировался бы: int i = 42; cin << i; // этот код был бы допустим, если бы преобразование
    // в тип bool не было явным!
    Эта программа пытается использовать оператор вывода для входного потока. Для класса istream оператор << не определен, поэтому такой код безусловно ошибочен. Но этот код мог бы использовать оператор преобразования в тип bool, чтобы преобразовать объект cin в bool. Полученное значение типа bool было бы затем преобразовано в тип int, который вполне применим как левый операнд встроенной версии оператора сдвига влево. В результате преобразованное значение типа bool (1 или 0) было бы сдвинуто влево на 42 позиции. Явный оператор преобразования
    Чтобы предотвратить подобные проблемы, новый стандарт вводит явный оператор преобразования (explicit conversion operator): class SmallInt { public:
    // компилятор не будет автоматически применять это преобразование explicit operator int() const { return val; }
    // другие члены как прежде
    };
    Подобно явным конструкторам (см. раздел 7.5.4), компилятор не будет (обычно) использовать явный оператор преобразования для неявных преобразований:
    SmallInt si = 3; // ok: конструктор класса SmallInt не является явным si + 3; //
    Page 729/1103
    ошибка: нужно неявное преобразование, но оператор int
    // является явным static_cast<int>(si) + 3; // ok: явный запрос преобразования
    Если оператор преобразования является явным, такое преобразование вполне можно осуществить. Но за одним исключением такое приведение следует осуществить явно.
    Исключение состоит в том, что компилятор применит явное преобразование в выражении,
    используемом как условие. Таким образом, явное преобразование будет использовано неявно для преобразования выражения, используемого как:
    • условие оператора if, while или do;
    • выражение условия в заголовке оператора for;
    • операнд логического оператора NOT (!), OR (||) или AND (&&);
    • выражение условия в условном операторе (?:). Преобразование в тип bool
    В прежних версиях библиотеки типы ввода-вывода определяли преобразование в тип void*.
    Это было сделано во избежание проблем, описанных выше. По новому стандарту библиотека ввода-вывода определяет вместо этого явное преобразование в тип bool.
    Всякий раз, когда потоковый объект используется в условии, применяется оператор operator bool(), определенный для типов ввода-вывода. Например: while (std::cin >> value)
    Условие в операторе while выполняет оператор ввода, который читает в переменную value и возвращает объект cin. Для обработки условия объект cin неявно преобразуется функцией преобразования istream::operator bool(). Эта функция возвращает значение true, если флагом состояния потока cin является good (см. раздел 8.1.2), и false в противном случае.
    Преобразование в тип bool обычно используется в условиях. В результате оператор operator bool обычно должен определяться как явный. Упражнения раздела 14.9.1
    Упражнение 14.45. Напишите операторы преобразования для преобразования объекта класса Sales_data в значения типа string и double. Какие значения, по-вашему, должны возвращать эти операторы?
    Упражнение 14.46. Объясните, является ли определение этих операторов преобразования класса Sales_data хорошей идеей и должны ли они быть явными.
    Упражнение 14.47. Объясните различие между этими двумя операторами преобразования: struct Integral { operator const int(); operator int() const;
    };
    Page 730/1103

    Упражнение 14.48. Должен ли класс из упражнения 7.40 раздела 7.5.1 использовать преобразование в тип bool. Если да, то объясните почему и укажите, должен ли оператор быть явным. В противном случае объясните, почему нет.
    Упражнение 14.49. Независимо от того, хороша ли эта идея, определите преобразование в тип bool для класса из предыдущего упражнения.
    14.9.2. Избегайте неоднозначных преобразований
    Если у класса есть один или несколько операторов преобразования, важно гарантировать наличие только одного способа преобразования из типа класса в необходимый тип. Если будет больше одного способа осуществления преобразования, то будет весьма затруднительно написать однозначный код.
    Есть два случая, когда возникает несколько путей осуществления преобразования. Первый —
    когда два класса обеспечивают взаимное преобразование. Например, взаимное преобразование осуществляется тогда, когда класс А определяет конструктор преобразования, получающий объект класса B, а класс в определяет оператор преобразования в тип А.
    Второй случай возникновения нескольких путей преобразования — определение нескольких преобразований в и из типов, которые сами связаны преобразованиями. Самый очевидный пример — встроенные арифметические типы. Каждый класс обычно должен определять не больше одного преобразования в или из арифметического типа.
    Обычно не следует определять классы со взаимными преобразованиями или определять преобразования в или из арифметических типов. Распознавание аргумента и взаимные преобразования
    В следующем примере определены два способа получения объекта класса А из В: либо при помощи оператора преобразования класса В, либо при помощи конструктора класса А,
    получающего объект класса В:
    // обычно взаимное преобразование между двумя типами - плохая идея struct B; struct А {
    А() = default;
    A(const В&); // преобразует В в A
    // другие члены
    }; struct В {
    Page 731/1103
    operator A() const; // тоже преобразует В в A
    // другие члены
    };
    A f (const A&);
    A a = f(b); // ошибка неоднозначности: f(B::operator A())
    // или f(A::A(const B&))
    Поскольку существуют два способа получения объекта класса А из В, компилятор не знает,
    какой из них использовать; поэтому вызов функции f() неоднозначен. Для получения объекта класса В этот вызов может использовать конструктор класса А или оператор преобразования класса В, преобразующий объект класса В в А. Поскольку обе эти функции одинаково хороши, вызов неоднозначен и ошибочен.
    Если этот вызов необходим, оператор преобразования или конструктор следует вызвать явно:
    A a1 = f(b.operator А()); // ok: использовать оператор преобразования В
    A а2 = f(A(b)); // ok: использовать конструктор класса А
    Обратите внимание: нельзя решить неоднозначность при помощи приведения — у самого приведения будет та же двусмысленность. Двусмысленность и множественность путей преобразования во встроенные типы
    Двусмысленность возникает также в случае, когда класс определяет несколько преобразований в (или из) типы, которые сами связываются преобразованиями. Самый простой и наглядный пример (а также особенно проблематичный) — это когда класс определяет конструкторы преобразования в или из более, чем один арифметический тип.
    Например, у следующего класса есть конструкторы преобразования из двух разных арифметических типов и операторы преобразования в два разных арифметических типа: struct A {
    A(int = 0); // обычно плохая идея иметь два
    A(double); // преобразования из арифметических типов
    Page 732/1103
    operator int() const; // обычно плохая идея иметь два operator double() const; // преобразования в арифметические типы
    // другие члены
    }; void f2(long double);
    A a; f2(a); // ошибка неоднозначности: f(A::operator int())
    // или f (A::operator double ()) long lg;
    A a2(lg); // ошибка неоднозначности: A::A(int) или A::A(double)
    В вызове функции f2() ни одно из преобразований не соответствует точно типу long double. Но для его получения применимо любое преобразование, сопровождаемое стандартным преобразованием. Следовательно, никакое из преобразований не лучше другого, значит,
    вызов неоднозначен.
    Возникает та же проблема, что и при попытке инициализации объекта a2 значением типа long. Ни один из конструкторов не соответствует точно типу long. Каждый требовал преобразования аргумента прежде, чем использовать конструктор.
    • Стандартное преобразование long в double, затем A(double).
    • Стандартное преобразование long в int, затем A(int).
    Эти последовательности преобразований равнозначны, поэтому вызов неоднозначен.
    Вызов функции f2() и инициализация объекта a2 неоднозначны, поскольку у необходимых стандартных преобразований одинаковый ранг (см. раздел 6.6.1). Когда используется пользовательское преобразование, ранг стандартного преобразования, если таковые вообще имеются, позволяет выбрать наилучшее соответствие: short s = 42;
    // преобразование short в int лучше, чем short в double
    А a3(s); // используется A::A(int)
    Page 733/1103

    В данном случае преобразование short в int предпочтительней, чем short в double.
    Следовательно, объект a3 создается с использованием конструктора A::A(int), который запускается для преобразования значения s.
    Когда используются два пользовательских преобразования, ранг стандартного преобразования, если таковое вообще имеется, используется для выбора наилучшего соответствия. Перегруженные функции и конструкторы преобразования
    Выбор из нескольких возможных преобразований еще более усложняется, когда происходит вызов перегруженной функции. Если два или более преобразования обеспечивают подходящее соответствие, то преобразования считаются одинаково хорошими.
    Например, могут возникнуть проблемы неоднозначности, когда перегруженные функции получают параметры, отличающиеся типами классов, которые определяют те же конструкторы преобразования: Внимание! Преобразования и операторы
    Корректная разработка перегруженных операторов, конструкторов преобразования и функций преобразования для класса требует большой осторожности. В частности, если в классе определены и операторы преобразования, и перегруженные операторы, вполне возможны неоднозначные ситуации. Здесь могут пригодиться следующие эмпирические правила.
    • Никогда не создавайте взаимных преобразований типов. Другими словами, если класс Foo имеет конструктор, получающий объект класса Bar, не создавайте в классе Bar оператор преобразования для типа Foo.
    • Избегайте преобразований во встроенные арифметические типы. Но если преобразование в арифметический тип необходимо, то придется учесть следующее.
    - Не создавайте перегруженных версий тех операторов, которые получают аргументы арифметических типов. Если пользователи используют эти операторы, функция преобразования преобразует объект данного типа, а затем применит встроенный оператор.
    - Не создавайте функций преобразования больше, чем в один арифметический тип.
    Позвольте осуществлять преобразования в другие арифметические типы стандартным функциям преобразования.
    Самое простое правило: за исключением явного преобразования в тип bool, избегайте создания функций преобразования и ограничьте неявные конструкторы теми, которые безусловно необходимы. struct C {
    C(int);
    // другие члены
    }; struct D {
    D(int);
    // другие члены
    Page 734/1103

    }; void manip(const С&); void manip(const D&); manip(10); // ошибка неоднозначности: manip(С(10)) или manip(D(10))
    Здесь у структур С и D есть конструкторы, получающие значение типа int. Для версий функции manip() подходит любой конструктор. Следовательно, вызов неоднозначен: он может означать преобразование int в С и вызов первой версии manip() или может означать преобразование int в D и вызов второй версии.
    Вызывающая сторона может устранить неоднозначность при явном создании правильного типа: manip(С(10)); // ok: вызов manip(const C&)
    Необходимость в использовании конструктора или приведения для преобразования аргумента при обращении к перегруженной функции — это признак плохого проекта.
    Перегруженные функции и пользовательские преобразования
    Если при вызове перегруженной функции два (или больше) пользовательских преобразования обеспечивают подходящее соответствие, они считаются одинаково хорошими. Ранг любых стандартных преобразований, которые могли бы (или не могли) быть обязательными, не рассматриваются. Необходимость встроенного преобразования также рассматривается, только если набор перегруженных версий может быть подобран и использован той же функцией преобразования .
    Например, вызов функции manip() был бы неоднозначен, даже если бы один из классов определил конструктор, который требовал бы для аргумента стандартного преобразования: struct E {
    E(double);
    // другие члены
    }; void manip2(const C&); void manip2(const E&);
    // ошибка неоднозначности: применимы два разных пользовательских
    // преобразования
    Page 735/1103
    manip2(10); // manip2(C(10) или manip2(E(double(10)))
    В данном случае у класса С есть преобразование из типа int и у класса E есть преобразование из типа double. Для вызова manip2(10) подходят обе версии функции manip2():
    • Версия manip2(const C&) подходит потому, что у класса С есть конструктор преобразования, получающий тип int. Этот конструктор точно соответствует аргументу.
    • Версия manip2(const E&) подходит потому, что у класса E есть конструктор преобразования, получающий тип double и возможность использовать стандартное преобразование для преобразования аргумента типа int, чтобы использовать этот конструктор преобразования.
    Поскольку вызовы перегруженных функций требуют разных пользовательских преобразований друг от друга, этот вызов неоднозначен. В
    частности, даже при том, что один из вызовов требует стандартного преобразования, а другой является точным соответствием, компилятор все равно отметит этот вызов как ошибку.
    Ранг дополнительного стандартного преобразования (если оно есть) при вызове перегруженной функции имеет значение, только если подходящие функции требуют того же пользовательского преобразования. Если необходимы разные пользовательские преобразования, то вызов неоднозначен. Упражнения раздела 14.9.2
    Упражнение 14.50. Представьте возможные последовательности преобразований типов для инициализации объектов ex1 и ex2. Объясните, допустима ли их инициализация или нет. struct LongDouble {
    LongDouble(double = 0.0); operator double(); operator float();
    };
    LongDouble IdObj; int ex1 = IdObj; float ex2 = IdObj;
    Упражнение 14.51. Представьте последовательности преобразования (если они есть),
    необходимые для вызова каждой версии функции calc(), и объясните, как подбирается наилучшая подходящая функция. void calc(int); void calc(LongDouble); double dval; calc(dval); //
    Page 736/1103
    которая calc()?
    14.9.3. Подбор функций и перегруженные операторы
    Перегруженные операторы — это перегруженные функции. При выявлении, который из встроенных или перегруженных операторов применяется для данного выражения,
    используется обычный подбор функции (см. раздел 6.4). Однако, когда в выражении используется функция оператора, набор функций-кандидатов шире, чем при вызове функций,
    использующих оператор вызова. Если объект а имеет тип класса, то выражение a sym b может быть следующим: a.operator sym (b); // класс а содержит оператор sym как функцию-член operator sym (a, b); // оператор sym - обычная функция
    В отличие от обычных вызовов функции, нельзя использовать форму вызова для различения функции-члена или не члена класса.
    Когда используется перегруженный оператор с операндом типа класса, функции-кандидаты включают обычные версии, не являющиеся членами класса этого оператора, а также его встроенные версии. Кроме того, если левый операнд имеет тип класса, определенные в нем перегруженные версии оператора (если они есть) также включаются в набор кандидатов.
    Когда вызывается именованная функция, функции-члены и не члены класса с тем же именем не перегружают друг друга. Перегрузки нет потому, что синтаксис, используемый для вызова именованной функции, различает функции- члены и не члены класса. При вызове через объект класса (или ссылку, или указатель на такой объект) рассматриваются только функции-члены этого класса. При использовании в выражении перегруженного оператора нет никакого способа указать на использование функции-члена или не члена класса. Поэтому придется рассматривать версии и функции-члены, и не члены класса.
    Набор функций-кандидатов для используемого в выражении оператора может содержать функции-члены и не члены класса.
    Определим, например, оператор суммы для класса SmallInt: class SmallInt { friend
    SmallInt operator*(const SmallInt&, const SmallInt&); public:
    Page 737/1103

    SmallInt(int = 0); // преобразование из int operator int() const { return val; } // преобразование в int private: std::size_t val;
    };
    Этот класс можно использовать для суммирования двух объектов класса SmallInt, но при попытке выполнения смешанных арифметических операций возникнет проблема неоднозначности:
    SmallInt s1, s2;
    SmallInt s3 = s1 + s2; // использование перегруженного оператора + int i = s3 + 0; // ошибка: неоднозначность
    Первый случай суммирования использует перегруженную версию оператора + для суммирования двух значений типа SmallInt. Второй случай неоднозначен, поскольку 0 можно преобразовать в тип SmallInt и использовать версию оператора + класса SmallInt либо преобразовать объект s3 в тип int и использовать встроенный оператор суммы для типа int.
    Предоставление функции преобразования в арифметический тип и перегруженных операторов для того же типа может привести к неоднозначности между перегруженными и встроенными операторами. Упражнения раздела 14.9.3
    Упражнение 14.52. Какой из операторов operator+, если таковые вообще имеются, будет выбран для каждого из следующих выражений суммы? Перечислите функции-кандидаты,
    подходящие функции и преобразования типов для аргументов каждой подходящей функции: struct LongDouble {
    // оператор-член operator+ только для демонстрации;
    // обычно он не является членом класса
    LongDouble operator+(const SmallInt&); // другие члены как в p. 14.9.2
    };
    LongDouble operator+(LongDouble&, double);
    Page 738/1103

    SmallInt si;
    LongDouble ld; ld = si + ld; ld = ld + si;
    Упражнение 14.53. С учетом определения класса SmallInt определите, допустимо ли следующее выражение суммы. Если да, то какой оператор суммы используется? В противном случае, как можно изменить код, чтобы сделать его допустимым?
    SmallInt s1; double d = s1 + 3.14;
    Резюме
    Перегруженный оператор должен либо быть членом класса, либо иметь по крайней мере один операнд типа класса. У перегруженных операторов должно быть то же количество операндов, порядок и приоритет, как у соответствующего оператора встроенного типа. Когда оператор определяется как член класса, его неявный указатель this связан с первым операндом. Операторы присвоения, индексирования, вызова функции и стрелки должны быть членами класса.
    Объекты классов, которые перегружают оператор вызова функции, operator() называются "объектами функций". Такие объекты зачастую используются в комбинации со стандартными алгоритмами. Лямбда-выражения — это отличный способ определения простых классов объектов функции.
    Класс может определить преобразования в или из своего типа, которые будут использованы автоматически. Неявные конструкторы, которые могут быть вызваны с одним аргументом,
    определяют преобразования из типа параметра в тип класса; операторы неявного преобразования определяют преобразования из типа класса в другие типы.
    Термины
    Объект функции (function object). Объект класса, в котором определен перегруженный оператор вызова. Объекты функций применяются там, где обычно ожидаются функции.
    Оператор преобразования (conversion operator). Оператор преобразования — это функция-член, которая осуществляет преобразование из типа класса в другой тип.
    Операторы преобразования должны быть константными членами их класса. Такие функции не получают параметров и не имеют типа возвращаемого значения. Они возвращают значение типа оператора преобразования. То есть оператор operator int возвращает тип int,
    оператор operator string — тип string и т.д.
    Перегруженный оператор (overloaded operator). Функция, переопределяющая значение одного из встроенных операторов. Функция перегруженного оператора имеет имя operator с
    Page 739/1103
    последующим определяемым символом. У перегруженных операторов должен быть по крайней мере один операнд типа класса. У перегруженных операторов тот же приоритет,
    порядок и количество операндов, что и у их встроенных аналогов.
    Пользовательское преобразование (user-defined conversion). Синоним термина преобразование типа класса.
    Преобразование типа класса (class-type conversion). Преобразования в или из типа класса определяются конструкторами и операторами преобразования соответственно. Неявные конструкторы, получающие один аргумент, определяют преобразование из типа аргумента в тип класса. Операторы преобразования определяют преобразования из типа класса в заданный тип.
    Сигнатура вызова (call signature). Представляет интерфейс вызываемого объекта. Сигнатура вызова включает тип возвращаемого значения и заключенный в круглые скобки разделяемый запятыми список типов аргументов.
    Таблица функций (function table). Контейнер, как правило, карта или вектор, содержащий значения, позволяющие выбрать и выполнить функцию во время выполнения.
    Шаблон функции (function template). Библиотечный шаблон, способный представить любой вызываемый тип.
    Явный оператор преобразования (explicit conversion operator). Оператор преобразования с предшествующим ключевым словом explicit. Такие операторы используются для неявных преобразований только в определенных условиях.
    Глава 15
    Объектно-ориентированное программирование
    Объектно-ориентированное программирование основано на трех фундаментальных концепциях: абстракция данных, наследование и динамическое связывание.
    Наследование и динамическое связывание рационализируют программы двумя способами:
    они упрощают создание новых классов, которые подобны, но не идентичны другим классам, а также облегчают написание программы, позволяя игнорировать незначительные различия в подобных классах.
    При создании большинства приложений используются одинаковые принципы, которые различаются лишь способами их реализации. Например, рассматриваемый для примера книжный магазин мог бы применять различные системы тарификации для разных книг.
    Некоторые книги можно было бы продавать лишь по фиксированной цене, а для других применить гибкую систему скидок. Можно было бы предоставлять скидку тем покупателям,
    которые покупают несколько экземпляров книги. Скидку можно было бы также предоставить на несколько первых экземпляров, а для остальных оставить полную цену.
    Объектно-ориентированное программирование (Object-Oriented Programming, или ООП) —
    это наилучший способ создания приложений такого типа.
    15.1. Краткий обзор ООП
    Page 740/1103

    Ключевыми концепциями объектно-ориентированного программирования являются абстракция данных, наследование и динамическое связывание. Используя абстракцию данных, можно определить классы, отделяющие интерфейс от реализации (см. главу 7).
    Наследование позволяет определять классы, моделирующие отношения между подобными типами. Динамическое связывание позволяет использовать объекты этих типов, игнорируя незначительные различия между ними. Наследование
    Связанные наследованием (inheritance) классы формируют иерархию. В корне иерархии обычно находится базовый класс (base class), от которого прямо или косвенно происходят другие классы. Эти унаследованные классы известны как производные классы (derived class). В базовом классе определяют те члены, которые будут общими у всех типов в иерархии. В производных классах определяются те члены, которые будут специфическими для данного производного класса.
    Для моделирования разных стратегий расценок определим класс Quote, который будет базовым классом нашей иерархии. Объект класса Quote представит книгу без скидок. От него унаследуем второй класс, Bulk_quote, представляющий книги, которые могут быть проданы со скидкой за опт.
    У этих классов будут две функции-члена.
    • Функция isbn() будет возвращать ISBN. Она никак не зависит от специфических особенностей производных классов; поэтому будет определена только в классе Quote.
    • Функция net_price(size_t) будет возвращать цену при покупке определенного количества экземпляров книги. Эта операция специфична для типа; классы Quote и Bulk_quote определят собственные версии этой функции.
    В языке С++ базовый класс отличает функции, специфические для типа, от тех, которые предполагается наследовать в производных классах без изменений. Те функции, которые производные классы должны определять самостоятельно, базовый класс определяет как virtual. Исходя из этого, класс Quote можно первоначально написать так: class Quote { public: std::string isbn() const; virtual double net_price(std::size_t n) const;
    };
    Производный класс должен указать класс (классы), который он намеревается унаследовать.
    Для этого используется находящийся после двоеточия список наследования класса (class derivation list), представляющий собой разделяемый запятыми список базовых классов, у каждого из которых может быть необязательный спецификатор доступа:
    Page 741/1103
    class Bulk_quote : public Quote { //
    Bulk_quote наследуется от Quote public: double net_price(std::size_t) const override;
    };
    Поскольку класс Bulk_quote использует в списке наследования спецификатор public, его объекты можно использовать так, как будто они являются объектами класса Quote.
    Тело производного класса должно включать объявления всех виртуальных функций (virtual function), которые он намеревается определить для себя.
    Производный класс может включить в эти функции ключевое слово virtual, но не обязательно.
    По причинам, рассматриваемым в разделе 15.3, новый стандарт позволяет производному классу явно указать, что функция-член предназначена для переопределения (override) унаследованной виртуальной функции. Для этого после списка ее параметров располагают ключевое слово override.Динамическое связывание
    Динамическое связывание (dynamic binding) позволяет взаимозаменяемо использовать тот же код для обработки объектов как типа Quote, так и Bulk_quote. Например, следующая функция выводит общую стоимость при покупке заданного количества экземпляров указанной книги:
    // вычислить и отобразить цену за указанное количество экземпляров
    // с применением всех скидок double print_total(ostream &os, const Quote &item, size_t n) {
    // в зависимости от типа, связанного с параметром item объекта,
    // вызвать функцию Quote::net_price() или Bulk_quote::net_price() double ret = item.net_price(n); os << "ISBN: " << item.isbn() // вызов Quote::isbn()
    << " # sold: " << n << " total due: " << ret << endl; return ret;
    Page 742/1103

    }
    Эта функция довольно проста — она выводит результаты вызова функций isbn() и net_price()
    для своего параметра и возвращает значение, вычисленное вызовом функции net_price().
    Однако у этой функции есть два интересных момента: по описанным в разделе 15.2.3
    причинам, поскольку параметр item является ссылкой на тип Quote, эту функцию можно вызвать как для объекта класса Quote, так и для объекта класса Bulk quote. По причинам,
    описанным в разделе 15.2.1, поскольку функция net_price() является виртуальной, а функция print_total() вызывает ее через ссылку, выполняемая версия функции net_price() будет зависеть от типа объекта, переданного функции print_total():
    // basic имеет тип Quote; bulk имеет тип Bulk_quote print_total(cout, basic, 20); // вызов версии net_price() класса Quote print_total(cout, bulk, 20); // вызов версии net_price()
    // класса Bulk_quote
    Первый вызов передает функции print_total() объект класса Quote. Когда функция print_total()
    вызовет функцию net_price(), будет выполнена ее версия из класса Quote. В следующем вызове, где аргумент имеет тип Bulk_quote, будет выполнена версия функции net_price() из класса Bulk_quote (применяющая скидку). Поскольку решение о выполняемой версии зависит от типа аргумента, оно может быть принято до времени выполнения. Поэтому динамическое связывание иногда называют привязкой во время выполнения (run-time binding).
    В языке С++ динамическое связывание происходит тогда, когда обращение к виртуальной функции осуществляется при помощи ссылки (или указателя) на базовый класс.
    15.2. Определение базовых и производных классов
    Во многих, но не всех случаях базовые и производные классы определяются, как и другие классы, но отличия все же имеются. В этом разделе рассматриваются основные возможности, используемые при определении классов, связанных наследованием.
    15.2.1. Определение базового класса
    Для начала завершим определение класса Quote:
    Page 743/1103
    class Quote { public:
    Quote() = default; //
    = default см. раздел 7.1.4
    Quote(const std::string &book, double sales_price): bookNo(book), price(sales_price) { } std::string isbn() const { return bookNo; }
    // возвращает общую цену за определенное количество проданных
    // экземпляров, а различные системы скидок определяют и
    // применяют производные классы virtual double net_price(std::size_t n) const
    { return n * price; } virtual

    Quote() = default; // динамическое связывание для
    // деструктора private: std::string bookNo; // идентификатор экземпляра protected: double price = 0.0; // стандартная цена (без скидки)
    };
    Новым в этом классе являются использование ключевого слова virtual в функции net_price() и деструкторе, а также спецификатора доступа protected. Виртуальные деструкторы рассматриваются в разделе
    15.7.1, а пока следует заметить, что корневой класс иерархии наследования почти всегда определяет виртуальный деструктор.
    Page 744/1103

    Базовые классы обычно должны определять виртуальный деструктор. Виртуальные деструкторы необходимы, даже если они не делают ничего. Функции-члены и наследование
    Производные классы наследуют члены своих базовых классов. Но производный класс должен быть в состоянии обеспечить собственное определение таких зависимых от типа операций,
    как net_price(). В таких случаях производный класс должен переопределить унаследованное от базового класса определение, обеспечив собственное определение.
    В языке С++ базовый класс должен отличать функции, которые предполагается переопределить в производных классах, от тех, которые производные классы, вероятно,
    наследуют без изменений. Функции, переопределение которых предполагается в производных классах, базовый класс определяет как virtual. Когда вызов виртуальной функции происходит через указатель или ссылку , он будет привязан динамически. В зависимости от типа объекта,
    с которым связана ссылка или указатель, будет выполнена версия базового или одного из его производных классов.
    Базовый класс определяет, что функция-член должна быть привязана динамически,
    предваряя ее объявление ключевым словом virtual. Любая нестатическая функция-член (см.
    раздел 7.6), кроме конструктора, может быть виртуальной. Ключевое слово virtual присутствует только в объявлении в классе и не может использоваться в определении функции вне тела класса. Функция, объявленная виртуальной в базовом классе, неявно является виртуальной и в производных классах. Более подробная информация о виртуальных функциях приведена в разделе 15.3.
    Функции-члены, которые не объявлены как virtual, распознаются во время компиляции, а не во время выполнения. Это именно то поведение, которое необходимо для функции isbn().
    Она не зависит от подробностей производного типа и ведет себя одинаково как с объектами класса Quote, так и Bulk_quote. В нашей иерархии наследования будет только одна версия функции isbn(). Таким образом, не будет никаких вопросов относительно выполняемой версии функции isbn() при вызове. Управление доступом и наследование
    Производный класс наследует члены, определенные в его базовом классе. Но функции-члены производного класса не обязаны обращаться к членам, унаследованным от базового класса. Подобно любому другому коду, использующему базовый класс,
    производный класс может обращаться к открытым членам своего базового класса, но не может обратиться к закрытым членам. Но иногда у базового класса могут быть члены,
    которые следует позволить использовать в производных классах, но все же запретить доступ к ним другим пользователям. В определении таких членов используется спецификатор доступа protected.
    Класс Quote ожидает, что его производные классы определят собственную функцию net_price(). Для этого им потребуется доступ к члену price. В результате класс Quote определяет эту переменную-член как protected. Производные классы получат доступ к переменной bookNo таким же образом, как и обычные пользователи, — при вызове функции isbn(). Следовательно, переменная-член bookNo останется закрытой и недоступной классам,
    производным от класса Quote. Более подробная информация о защищенных членах приведена в разделе 15.5. Упражнения раздела 15.2.1

    1   ...   35   36   37   38   39   40   41   42   ...   54


    написать администратору сайта