В этой главе рассматриваются некоторые аспекты 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.