В этой главе рассматриваются некоторые аспекты C++, которые плохо вписываются в тематику любой другой главы: указатели функций и членов, константные переменные и функции- члены, независимые операторы (т.е. не члены класса) и несколько других тем.
Планируется использование некоторой функции
func1, которая на этапе выполнения должна вызывать другую функцию func2. Однако по той или иной причине нельзя внутри функции func1 жестко закодировать имя функции func2. Возможно, имя функции func2 неизвестно на этапе компиляции, или func1 относится к программному интерфейсу независимого разработчика, и она не может быть изменена и перекомпилирована В любом случае вам придется воспользоваться функцией обратного вызова (callback function).
При использовании указанных выше функций объявите
func1 с указателем на функцию в качестве своего аргумента и передайте ей адрес func2 на этапе выполнения. Используйте typedef, чтобы программа легче читалась и отлаживалась. Пример 15.1 показывает, как можно реализовать функцию обратного вызова, используя указатель на функцию.
Пример 15.1. Функция обратного вызова
#include
// Пример функции обратного вызова
bool updateProgress(int pct) {
std::cout << pct << "% complete...\n";
return(true);
}
// Этот typedef делает программный код более понятным
typedef bool (*FuncPtrBoolInt)(int);
// Функция, которая выполняется достаточно длительное время
void longOperation(FuncPtrBoolInt f) {
for (long l=0; l < 100000000; l++)
if (l % 10000000 == 0)
f(l/1000000);
}
int main() {
longOperation(updateProgress); // нормально
}
В ситуации, которая показана в примере 15.1, применение указателя на функцию является хорошим решением, если
UpdateProgress и longOperation ничего не должны знать друг о друге. Например, функцию, которая обновляет индикатор состояния процесса в диалоговом окне пользовательского интерфейса (user interface — UI), в окне консольного режима или где-то еще, не заботит контекст, в котором она вызывается. Аналогично функция longOperation может быть частью некоторого программного интерфейса загрузки данных, которого не заботит место вызова: из графического UI, из окна консольного режима или из фонового процесса.
Сначала потребуется определить сигнатуру функции, которую вы планируете вызывать, и создать для нее
typedef. Оператор typedef — ваш помощник в тех случаях, когда приходится иметь дело с указателями функций, потому что они имеют не очень привлекательный синтаксис. Рассмотрим, как обычно объявляется такой указатель на примере переменной f, которая содержит адрес функции, принимающей единственный аргумент целого типа и возвращающей значения типа boolean. Это может выглядеть следующим образом
bool (*f)(int); // f - имя переменной
Вы можете справедливо возразить, что здесь нет ничего особенного и я просто излишне драматизирую ситуацию. Но что вы скажете, если требуется определить вектор
vector таких указателей?
vector vf;
Или их массив?
bool (*af[10])(int);
Форма представления указателей на функции отличается от обычных переменных С++, которые обычно задаются в виде (квалифицированного) имени типа, за которым идет имя переменной. Поэтому они вносят путаницу при чтении программного кода.
Итак, в примере 15.1 я использовал следующий
typedef.
typedef bool (*FuncPtrBoolInt)(int);
Сделав это, я могу свободно объявлять указатели функций с сигнатурой, возвращающей значение
bool и принимающей единственный аргумент, как это я бы делал для параметра любого другого типа, например.
void longOperation(FuncPtrBoolInt f) { // ...
Теперь все, что надо сделать в
longOperation, — это вызвать f, как если бы это была любая обычная функция.
f(l/1000000);
Таким образам, здесь
f может быть любой функцией, которая принимает аргумент целого типа и возвращает bool. Предположим, что в вызывающей функции longOperation не требуется обеспечивать продвижение индикатора состояния процесса. Тогда ей можно передать указатель на функцию без операций.
bool whoCares(int i) {return(true);}
//...
longOperation(whoCares);
Более важно то, что выбор функции, передаваемой
longOperation, может осуществляться динамически на этапе выполнения.
Требуется обеспечить адресную ссылку на данное-член или на функцию-член.
Используйте имя класса и оператор области видимости (
::) со звездочкой для правильного квалифицирования имени. Пример 15.2 показывает, как это можно сделать.
Пример 15.2. Получение указателя на член класса
#include
#include
class MyClass {
public:
MyClass() : ival_(0), sval_("foo") {}
~MyClass() {}
void incr() {++ival_;}
void decr() {ival_--;}
private:
std::string sval_;
int ival_;
};
int main() {
MyClass obj;
int MyClass::* mpi = &MyClass::ival_; // Указатели на
std::string MyClass::* mps = &MyClass::sval_; // данные-члены
void (MyClass::*mpf)(); // Указатель на функцию-член, у которой
// нет параметров и которая возвращает void
void (*pf)(); // Обычный указатель на функцию
int* pi = &obj.ival_; // int-указатель, ссылающийся на переменную-член
// типа int, - все нормально.
mpf = &MyClass::incr; // Указатель на функцию-член. Вы не можете
// записать это значение в поток. Посмотрите в
// отладчике, как это значение выглядит.
pf = &MyClass::incr; // Ошибка: &MyClass::inc не является экземпляром
// функции
std::cout << "mpi = " << mpi << '\n';
std::cout << "mps = " << mps << '\n';
std::cout << "pi = " << pi << '\n';
std::cout << "*pi = " << *pi << '\n';
obj.*mpi = 5;
obj.*mps = "bar";
(obj.*mpf)(); // теперь obj.ival_ равно 6
std::cout << "obj.ival_ = " << obj.ival_ << '\n';
std::cout << "obj.sval_ = " << obj.sval_ << '\n';
}
Указатели на члены класса выглядят и работают иначе, чем обычные указатели. Прежде всего, они имеют «смешной» синтаксис (не вызывающий смех, но странный). Рассмотрим следующую строку из примера 15.2.
int MyClass::* mpi = &MyClass::ival_;
Здесь объявляется указатель и ему присваивается значение целого типа, которым оказывается член класса
MyClass. Две вещи отнимают это объявление от обычного int*. Во-первых, вам приходится вставлять имя класса и оператор области видимости между типом данного и звездочкой. Во-вторых, при выполнении операции присваивания этому указателю на самом деле не назначается какой то определенный адрес памяти. Значение &MyClass::ival_ не является каким-то конкретным значением, содержащимся в памяти; оно ссылается на имя класса, а не на имя объекта, но тогда что же это такое на самом деле? Можно представить это значение как смешение данного-члена относительно начального адреса объекта.
Переменная
mpi должна использоваться совместно с экземпляром класса, к которому она применяется. Немного ниже в примере 15.2 располагается следующая строка, которая использует mpi для присваивания целого числа значению, на которое ссылается указатель mpi.
obj.*mpi = 5;
obj является экземпляром класса MyClass. Ссылка на член с использованием точки (или ->, если у вас имеется указатель на obj) и разыменование mpi позволяют вам получить ссылку на obj.ival_.
Указатели на функции-члены действуют фактически так же. В примере 15.2 объявляется указатель на функцию-член
MyClass, которая возвращает void и не имеет аргументов.
void (MyClass::*mpf)();
Ему можно присвоить значение с помощью оператора адресации.
mpf = &MyClass::incr;
Для вызова функции заключите основное выражение в скобки, чтобы компилятор понял ваши намерения, например:
(obj.*mpf)();
Однако имеется одно отличие в применении указателей на данные-члены и указателей на функции члены. Если необходимо использовать обычный указатель (не на член класса) на данное-член, просто действуйте обычным образом.
int* pi = &obj.ival_;
Конечно, вы используете имя объекта, а не имя класса, потому что получаете адрес конкретного данного-члена конкретного объекта, расположенного где-то в памяти. (Однако обычно стараются адреса данных-членов класса не выдавать за его пределы, чтобы нельзя было их изменить из-за опрометчивых действий в клиентском программном коде.)
В отличие от данного члена с функцией-членом вы не можете сделать то же самое, потому что это бессмысленно. Рассмотрим указатель на функцию, имеющую такую же сигнатуру, как
MyClass::incr (т.е. он возвращает void и не имеет аргументов).
void (*pf)();
Теперь попытайтесь присвоить этому указателю адрес функции-члена.
pf = &MyClass::incr; // He получится
pf = &obj.incr; // И это не пройдет
Обе эти строки не будут откомпилированы, и на это имеются веские основания. Применение функции-члена имеет разумный смысл только в контексте объекта, поскольку, вероятнее всего, она должна ссылаться на переменные-члены. Вызов функции-члена без объекта означало бы невозможность в функции-члене использовать какие-либо члены объекта, а эта функция, по-видимому, как раз является функцией-членом, а не автономной функцией, потому что использует члены объекта.
Рецепт 15.1.
Вы пишете функцию и требуется гарантировать, что ее аргументы не будут модифицированы при ее вызове.
Для предотвращения изменения аргументов вашей функцией объявите ее аргументы с ключевым словом
const. Короткий пример 15.3 показывает, как это можно сделать.
Пример 15.3. Гарантия невозможности модификации аргументов
#include
#include
void concat(const std::string& s1, // Аргументы объявлены как константное,
const std::string& s2, // поэтому не могут быть изменены
std::string& out) {
out = s1 + s2;
}
int main() {
std::string s1 = "Cabo ";
std::string s2 = "Wabo";
std::string s3;
concat(s1, s2, s3);
std::cout << "s1 = " << s1 << '\n';
std::cout << "s2 = " << s2 << '\n';
std::cout << "s3 = " << s3 << '\n';
}
В примере 15.3 продемонстрировано прямое использование ключевого слова
const. Существует две причины объявления параметров вашей функции с этим ключевым словом, когда вы не планируете их изменять. Во-первых, этим вы сообщаете о своих намерениях читателям вашего программного кода. Объявляя параметр как const, вы фактически говорите, что он является входным параметром. Это позволяет пользователям вашей функции писать программный код в расчете на то, что эти значения не будут изменены. Во-вторых, это позволяет компилятору запретить любые модифицирующие операции на тот случай, если вы случайно их используете. Рассмотрим небезопасную версию concat из примера 15 3.
void concatUnsafe(std::string& s1,
std::string& s2 std::string& out) {
out = s1 += s2; // Ну вот, записано значение в s1
}
Несмотря на мою привычку тщательно подходить к кодированию программ, я сделал глупую ошибку и написал
+= вместо +. В результате при вызове concatUnsafe будут модифицированы аргументы out и s1, что может оказаться сюрпризом для пользователя, который едва ли рассчитывает на модификацию одной из исходных строк.
Спасти может
const. Создайте новую функцию concatSafe, объявите переменные константными, как показано в примере 15.3, и функция не будет откомпилирована.
void concatSafe(const std::string& s1,
const std::string& s2, std::string& out) {
out = s1 += s2; // Теперь вы получите ошибку компиляции
}
concatSafе гарантирует неизменяемость значений в s1 и s2. Эта функция делает еще кое-что: она позволяет пользователю передавать константные аргументы. Например, программный код, выполняющий конкатенацию строк, мог бы выглядеть следующим образом.
void myFunc(const std::string& s) { // Обратите внимание, что s является
// константной переменной
std::string dest;
std::string tmp = "foo";
concatUnsafe(s, tmp, dest); // Ошибка: s - константная переменная
// Выполнить какие-то действия с dest...
}
В данном случае функция
myFunc не будет откомпилирована, потому что concatUnsafe не обеспечивает const'антность myFunc. myFunc гарантирует внешнему миру, что она не будет модифицировать содержимое s, т.е. все действия с s внутри тела myFunc не должны нарушать это обещание. Конечно, вы можете обойти это ограничение, используя оператор const_cast и тем самым освобождаясь от константности, но такой подход ненадежен, и его следует избегать. В этой ситуации concatSafe будет компилироваться и выполняться нормально.
Указатели вносят темные штрихи в розовую картину
const. Когда вы объявляете переменную-указатель как параметр, вы имеет дело с двумя объектами: самим адресом и то, на что ссылается этот адрес. C++ позволяет использовать const для ограничения действий по отношению к обоим объектам. Рассмотрим еще одну функцию конкатенации, которая использует указатели.
void concatUnsafePtr(std::string* ps1,
std::string* ps2, std::string* pout) {
*pout = *ps1 + *ps2;
}
Здесь такая же проблема, как в примере с
concatUnsafe, описанном ранее. Добавьте const для гарантии невозможности обновления исходных строк.
void concatSaferPtr(const std::string* ps1,
const std::string* ps2, std::string* pout) {
*pout = *ps1 + *ps2;
}
Отлично, теперь вы не можете изменить
*ps1 и *ps2. Но вы по-прежнему можете изменить ps1 и ps2, или, другими словами, используя их, вы можете сослаться на какую-нибудь другую строку, изменяя значение указателя, но не значение, на которое он ссылается. Ничто не может помешать вам, например, сделать следующее.
void concatSaferPtr(const std:string* ps1,
const std::string* ps2, std::string* pout) {
ps1 = pout; // Ух!
*pout = *ps1 + *ps2;
}
Предотвратить подобные ошибки можно с помощью еще одного
const.
void concatSafestPtr(const std::string* const ps1,
const std::string* const ps2, std::string* pout) {
*pout = *ps1 + *ps2;
}
Применение
const по обе стороны звездочки делает вашу функцию максимально надежной. В этом случае вы ясно показываете свои намерения пользователям вашей функции, и ваша репутация не пострадает в случае описки.
Рецепт 15.4.
Требуется вызывать функции -члены для константного объекта, но ваш компилятор жалуется на то, что он не может преобразовать тип используемого вами объекта из константного в неконстантный.
Поместите ключевое слово
const справа от имени функции-члена при ее объявлении в классе и при ее определении. Пример 15.4 показывает, как это можно сделать
Пример 15.4. Объявление функции-члена константной
#include
#include
class RecordSet {
public:
bool getFieldVal(int i, std::string& s) const;
// ...
};
bool RecordSet::getFieldVal(int i, std::string& s) const {
// Здесь нельзя модифицировать никакие неизменяемые
// данные-члены (см. обсуждение)
}
void displayRecords(const RecordSet& rs) {
// Здесь вы можете вызывать только константные функции-члены
// для rs
}
Добавление концевого
const в объявление члена и в его определение заставляет компилятор более внимательно отнестись к тому, что делается с объектом внутри тела члена. Константным функциям-членам не разрешается выполнять неконстантные операции с данными-членами. Если такие операции присутствуют, компиляция завершится неудачно. Например, если бы в RecordSet::getFieldVal я обновил счетчик-член, эта функция не была бы откомпилирована (в предположении, что getFieldCount_ является переменной-членом класса RecordSet).
bool RecordSet::getFieldVal(int i, std::string& s) const {
++getFieldCount_; // Ошибка: константная функция-член не может
// модифицировать переменную-член
// ...
}
Это может также помочь обнаружить более тонкие ошибки, подобно тому, что делает
const в роли квалификатора переменной (см. рецепт 15.3). Рассмотрим следующую глупую ошибку.
bool RecordSet::getFieldVal(int i, std::string& s) const {
fieldArray_[i] = s; // Ой, я не это имел в виду
// ...
}
Снова компилятор преждевременно завершит работу и выдаст сообщение об ошибке, потому что вы пытаетесь изменить переменную-член, а это не разрешается делать в константных функциях-членах. Ну, при одном исключении.
В классе
RecordSet (в таком, как (схематичный) класс в примере 15.4) вам, вероятно, потребовалось бы перемещаться туда-сюда по набору записей, используя понятие «текущей» записи. Простой способ заключается в применении переменной-члена целого типа, содержащей номер текущей записи; ваши функции-члены, предназначенные для перемещения текущей записи вперед-назад, должны увеличивать или уменьшать это значение.
void RecordSet::gotoNextPecord() const {
if (curIndex_ >= 0 && curIndex_ < numRecords_-1)
++curIndex_;
}
void RecordSet::gotoPrevRecord() const {
if (curIndex_ > 0)
--curIndex_;
}
Очевидно, что это не сработает, если эти функции-члены являются константными. Обе обновляют данное-член. Однако без этого пользователи класса
RecordSet не смогут перемещаться по объекту const RecordSet. Это исключение из правил работы с константными функциями-членами является вполне разумным, поэтому C++ имеет механизм его поддержки: ключевое слово mutable.
Для того чтобы
curIndex_ можно было обновлять в константной функции-члене, объявите ее с ключевым словом mutable в объявлении класса.
mutable int curIndex_;
Это позволит вам модифицировать
curIndex_ в любом месте. Однако этой возможностью следует пользоваться разумно, поскольку это действует на вашу функцию так, как будто она становится с этого момента неконстантной.
Применение ключевого слова
const в примере 15.4 позволяет гарантировать невозможность изменения состояния объекта в функции-члене. В целом, такой подход дает хорошие результаты, потому что сообщает пользователям класса о режиме работы функции-члена и потому что сохраняет вам репутацию, заставляя компилятор проконтролировать отсутствие в функции-члене непредусмотренных действий.
Необходимо написать бинарный оператор, и вы не можете или не хотите сделать его функцией-членом класса.
Используйте ключевое слово
operator, временную переменную и конструктор копирования для выполнения основной работы и возвратите временный объект. В примере 15.5 приводится простой оператор конкатенации строк для пользовательского класса String.
Пример 15.5. Конкатенация с использованием оператора не члена
#include
#include
class String { // Предположим, что объявление класса String содержит,
// по крайней мере, все, что указанно ниже
public:
String();
String(const char* p);
String(const String& orig);
~String() {delete buf_;}
String& append(const String& s);
size_t length() const;
const char* data() const;
String& operator=(const String& orig);
// ...
};
String operator+(const String& lhs, const String& rhs) {
String tmp(lhs); // Сконструировать временный объект с помощью
// конструктора копирования
tmp.append(rhs); // Использовать функцию-член для выполнения реальной
// работы
return(tmp); // Возвратить временный объект
}
int main() {
String s1("banana ");
String s2("rancher");
String s3, s4, s5, s6;
s3 = s1 + s2; // Работает хорошо, но с сюрпризами
s4 = s1 + "rama"; // Автоматически конструируется "rama", используя
// конструктор String(const char*)
s5 = "ham " + s2; // Круто, то же самое можно делать даже
s6 = s1 + "rama " + s2; // с другим операндом
std::cout << "s3 = " << s3.data() << '\n';
std::cout << "s4 = " << s4.data() << '\n';
std::cout << "s5 = " << s5.data() << '\n';
std::cout << "s6 = " << s6.data() << '\n';
}
Независимый оператор объявляется и определяется подобно оператору функции-члена. В примере 15.5 я мог бы реализовать
operator+ как функцию-член, объявляя ее следующим образом.
String operator+(const String& rhs);
В большинстве случаев это будет работать одинаково, независимо от того, определяется ли
operator+ как функция-член или нет, однако существует, по крайней мере, две причины, по которым желательно реализовать его не как функцию-член. Первая причина концептуальная, имеет ли смысл иметь оператор, который возвращает новый, отличный от других объект? operator+, реализованный как функция-член, не проверяет и не изменяет состояние объекта. Это служебная функция общего назначения, которая в данном случае работает со строками типа String и, следовательно, не должна являться функцией членом.
Вторая причина техническая. При использовании оператора-члена вы не сможете выполнить следующую операцию (из приведенного выше примера).
s5 = "ham " + s2;
Это не сработает, потому что символьная строка не имеет
operator+, который принимает String в качестве параметра. С другой стороны, если вы определили независимый operator+, который принимает два параметра типа String, ваш компилятор проверит наличие в классе String конструктора, принимающего const char* в качестве аргумента (или любой другой тип, который вы используете совместно с String), и сконструирует временный объект на этапе выполнения. Поэтому приведенная выше строка эквивалентна следующей.
s5 = String("ham ") + s2;
Компилятор позволяет вам немного сэкономить ваши действия и не вводить несколько символов за счет поиска и вызова соответствующего конструктора.
Перегрузка операторов сдвига потоков влево и вправо (
<< и >>) также требует применения операторов не-членов. Например, для записи нового объекта в поток, используя сдвиг влево, вам придется следующим образом объявить operator<<:
ostream& operator<<(ostream& str, const MyClass& obj);
Конечно, вы можете создать подкласс одного из классов потока стандартной библиотеки и добавить все необходимые вам операторы сдвига влево, но будет ли такое решение действительно удачным? При таком решении только тот программный код, который использует ваш новый класс потока, сможет записывать в него объекты вашего специального класса. Если вы используете независимый оператор, любой программный код в том же самом пространстве имен сможет без проблем записать ваш объект в
ostream (или считать его из istream).
Требуется инициализировать последовательность набором значений, разделяемых запятыми, подобно тому как это делается для встроенных массивов.
При инициализации стандартных последовательностей (таких как
vector и list) можно использовать синтаксис с запятыми, определяя вспомогательный класс и перегружая оператор запятой, как это продемонстрировано в примере 15.6.
Пример 15.6. Вспомогательные классы для инициализации стандартных последовательностей с применением синтаксиса с запятыми
#include
#include
#include
#include
using namespace std;
template
struct comma helper {
typedef typename Seq_T::value_type value_type;
explicit comma_helper(Seq_T& x) : m(x) {}
comma_helper& operator=(const value_type& x) {
m.clear();
return operator+=(x);
}
comma_helper& operator+=(const value_type& x) {
m.push_back(x);
return *this;
}
Seq_T& m;
};
template
comma_helper initialize(Seq_T& x) {
return comma_helper(x);
}
template
comma_helper& operator,(comma_helper& h, Scalar_T x) {
h += x;
return h;
}
int main() {
vector v;
int a = 2;
int b = 5;
initialize(v) = 0, 1, 1, a, 3, b, 8, 13;
cout << v[3] << endl; // выдает 2
system("pause");
return EXIT_SUCCESS;
}
Часто стандартные последовательности инициализируются путем вызова несколько раз функции-члена
push_back. Поскольку это приходится делать не так уж редко, я написал функцию initialize, которая помогает избавиться от этого скучного занятия, позволяя выполнять инициализацию значениями, разделяемыми запятыми, подобно тому как это делается во встроенных массивах.
Возможно, вы и не знали, что запятая является оператором, который можно переопределять. Здесь вы не одиноки — этот факт не является общеизвестным. Оператор запятой было разрешено перегружать почти только ради решения этой задачи.
В решении используется вспомогательная функция
initialize, которая возвращает шаблон вспомогательной функции comma_helper. Этот шаблон содержит ссылку на последовательность и перегруженные операторы operator,, operator= и operator+=.
Такое решение требует, чтобы я определил отдельную вспомогательную функцию из-за особенностей восприятия компилятором оператора
v = 1, 1, 2, ...;. Компилятор рассматривает v = 1 как недопустимое подвыражение, потому что в стандартных последовательностях не поддерживается оператор присваивания единственного значения. Функция initialize конструирует соответствующий объект comma_helper, который может хранить последовательность, используемую в перегруженном операторе присваивания и запятой.
Оператор запятой (comma operator), называемый также оператором последовательности (sequencing operator), по умолчанию рассматривает выражения слева направо, и в результате получается значение и тип самого правого значения. Однако при перегрузке
operator принимает новый смысл и теряет первоначальную семантику. Здесь возникает один тонкий момент — оценка параметров слева направо теперь не гарантируется, и результат выполнения программного кода, приведенного в примере 15.7, может оказаться неожиданным.
Пример 15.7. Применение перегруженного оператора запятой, когда порядок вычисления аргументов не определен
int prompt_user() {
cout << "give me an integer ... ";
cin >> n;
return n;
}
void f() {
vector v;
// Следующий оператор может инициализировать v в неправильной
// последовательности
intialize(v) = prompt_user(), prompt_user();
}
В правильном варианте функции
f каждый вызов prompt_user должен был бы выполняться в отдельном операторе.
Библиотека Boost Assign, написанная Торстеном Оттосеном (Thorsten Ottosen), кроме других форм инициализации стандартных коллекций поддерживает также более сложную форму инициализации списком с запятыми. Эта библиотека доступна на сайте http://www.boost.org.