Приведение типов в C++
-
- 1
- Эндрю Кениг, Барбара Му Эффективное программирование
на С++: Вильямс, 2002, 384 с.
- 2
- Стэнли Липпман Основы программирования на С++:
Вильямс, 2002, 256 с.
Замечательные вводные тексты, следующие канону Llama book. Неизвестно,
впрочем, насколько такой подход годится для C++ -- это ведь не Перл!
В любом случае, вводить vector в 1-ой главе -- правильная идея.
Книжка [1] забирается в дебри подальше.
- 3
- Бьерн Страуструп Язык программирования C++:
спец. изд. <<Издательство БИНОМ>> - <<Невский Диалект>>, 1099 с., 2001 г.
Классический ``источник знаний'' от автора языка и к тому же человека
более умного, чем заметное большинство ``пишущих программистов''.
Эпиграф достаточно чётко отражает ситуацию: прочитав абзац, хочется
подумать, что, увы, нечастая ситуация для программистских книжек.
Отдельные тёплые слова хотелось бы
произниести в адрес издателей русского перевода. Давно прошли времена,
когда Ершов переводил Брукса, но всё же перевод этой превосходной
книжки -- нечто завораживающее. Не часто встретишь ситуацию, когда
в тексте остаются два варианта перевода одного предложения, когда
приложения обозначаются русскими буквами, а часть ссылок в тексте
использует латиницу. Обнадёживает, впрочем, оставление части слов
в тексте непереведёнными -- как знак того, что мы будем читать
качественные оригиналы, а не переводы, подобные этому1. Перевод 2-го издание (ДиаСофт, 1993) сделан гораздо
качественнее, но вот только само 2-е издание устарело и рекомендовано
быть не может.
- 4
- Бьерн Страуструп Дизайн и эволюция языка C++:
М., ДМК Пресс, 2000, 448 с.
Книга содержит крайне полезное обсужение проектных решений, принятых
при создании C++. Помимо ответов на вопрос ``почему'', важных для
пишущих на C++, включает набор примеров и размышлений, общезначимых
и вне контекста ``разработка на C++''.
Ну, это, конечно, никакой не ``дизайн'', а ``проектирование''.
- 5
- Скотт Мейерс Эффективное использование STL:
СПб.: Питер, 2002, 224 с.
Такая ``поваренная книга'' из 50-и рецептов. К сожалению, при выбранной
форме изложения перемешивается то, что тривиально, и то, что нет.
- 6
- Андрей Александреску Современное проектирование
на С++: обобщённое программирование и прикладные шаблоны проектирования:
М., ``Вильямс'', 2002, 336 с.
После заглядывания внутрь STL (особенно см. окрестности type_traits.h
в STLPort), у меня всегда возникали 2 чувства:
- Я хочу так писать.
- Так писать я, по всей видимости, не могу, пока не приложу
спецусилий по восстановлению Мыслей из Исходников.
Так вот, в ч.I Александреску как раз и снимает покров тайн с.
Разумеется, глупо думать, что в каждой задаче неприменно нужно выделять
универсальные компоненты, параметризуемые стратегиями, или что проект,
не использующий Loki, никуда не годится.
Эта книга -- источник идей, а не учебник для кулинарного техникума.
Для начала рассмотрим полюбившийся пример с обработкой логов. Честно
говоря, особого желания показывать, как работать с регулярными выражениями,
нет2, и напротив, есть желание поразвлекаться
со структурами данных, потому задачка подвергнется переформулированию.
Наша цель -- написать анализатор логов, который бы искал типичные
паттерны использования web-сайта, т.е. характерные пути, по которым
пользователи ходят по сайту. Формально говоря, эта задача сводится к
нахождению повторяющихся подстрок. Решать эту задачу ``в лоб'' грустно,
применён приём, следующий Бентли Programming Pearls: чтобы похожие
хождения по сайту оказались рядом, массив хождений сортируется.
// logparser.cpp
#ifndef __GNUC__
#pragma warning (disable: 4786)
#pragma warning (disable: 4503)
#endif
#include <string>
#include <set>
#include <iostream>
#include <fstream>
#include <algorithm>
#ifdef __GNUC__
#include <ext/hash_map>
#else
#include <hash_map>
#endif
#include <bitset>
using namespace std;
void error_exit(const string &diag) {
cerr << diag << '\n';
exit(1);
}
/* split str on any of split_chars Copy in result only parts with flag ON.
Emplty fiels are preserved. */
vector<string> split(const string &str, const char *split_chars,
bitset<32> parts) {
vector<string> res;
string::size_type next_pos, curr_pos=0;
for (int i=0; ;i++) {
next_pos = str.find_first_of(split_chars, curr_pos);
if (parts[i])
res.push_back(str.substr(curr_pos, next_pos-curr_pos));
if (string::npos == next_pos)
break;
curr_pos = next_pos+1;
}
return res;
}
struct hash_string {
size_t operator() (const string &s) const {
#if 0
const char *_s = s.c_str();
unsigned long __h = 0;
for ( ; *_s; ++_s)
__h = 5*__h + *_s;
return __h;
#endif
return hash<const char*>()(s.c_str()); // direct call of () operator
}
};
template<class T> struct Printer{
void operator() (const T &t) {
cout << '\'' << t << '\'';
}
};
// max pause (in sec.) between reqs while its related to single session
static const int MAX_SESSION_PAUSE = 60*60;
struct SingleSessionInfo {
string ip; // can't be "const" due to ugly STL nature...
vector<string> urls;
};
typedef vector<SingleSessionInfo> AllSessions;
struct IP_URL_pair {
int sess_idx, // indexes in AllSessions
url_idx;
IP_URL_pair(int sess, int url) : sess_idx(sess), url_idx(url) {}
};
typedef vector<IP_URL_pair> AllReqs;
struct OpenSessionData { // need only while log parsing
int sess_idx;
long last_time;
OpenSessionData(int sess_idx, long last_time) : sess_idx(sess_idx),
last_time(last_time) {}
};
// key is ip-addr, val - history of URLs from it
#ifdef __GNUC__
typedef hash_map<string, OpenSessionData, hash_string> OpenSessions;
#else
typedef hash_map<string, OpenSessionData> OpenSessions;
#endif
const int HISTORY_SIZE = 30; // search only for history of this size
struct PatternData { // found data about usage patterns
vector<string> ips;
vector<string> urls;
PatternData() {}
PatternData(const string &ip) {
ips.push_back(ip);
}
};
class HistoryComparator {
const AllSessions &sess;
public:
HistoryComparator(const AllSessions &sess) : sess(sess) {}
bool operator() (const IP_URL_pair &p1, const IP_URL_pair &p2) const {
// not [], because it conflics with 'const'!
const vector<string> &v1 = sess[p1.sess_idx].urls;
const vector<string> &v2 = sess[p2.sess_idx].urls;
return lexicographical_compare(v1.begin()+p1.url_idx, v1.end(),
v2.begin()+p2.url_idx,
v2.end());
}
};
static AllSessions all_sessions; // need global data for <<
ostream& operator << (ostream &s, const IP_URL_pair &p) {
return s << '[' << all_sessions[p.sess_idx].ip <<' '<<
all_sessions[p.sess_idx].urls[p.url_idx] << "]\n";
}
ostream& operator << (ostream &s, const PatternData &p) {
for_each(p.ips.begin(), p.ips.end(), Printer<string>());
s << "\n------------------------\n";
for_each(p.urls.begin(), p.urls.end(), Printer<string>());
return s << "\n------------------------\n";
}
int main(int argc, char *argv[]) {
AllReqs all_reqs;
OpenSessions open_sessions;
vector<PatternData> usage_patts;
int curr_patt_index=-1;
string s, curr_line;
if (2 != argc)
error_exit(s + "Usage: "+argv[0]+" <file-with-log>");
ifstream logf(argv[1]);
if (! logf)
error_exit(s + "Can't open " + argv[1]);
while (! logf.eof()) {
getline(logf, curr_line);
const vector<string> res = split(curr_line, " ", 0x49);
if (3 != res.size())
continue;
const string ip_addr = res.at(0),
url = res.at(2);
const string time_str = res.at(1);
// 111 0010
const vector<string> dt_spl = split(time_str, "[/:", 0x72);
if (4 != dt_spl.size())
continue;
// ignore month et al there...
long sec = atoi(dt_spl[0].c_str())*24*60*60
+atoi(dt_spl[1].c_str())*60*60+atoi(dt_spl[2].c_str())*60
+atoi(dt_spl[3].c_str());
OpenSessions::iterator it;
int curr_sess_idx;
if (open_sessions.end() != (it = open_sessions.find(ip_addr))) {
// need for new session
if (sec - it->second.last_time > MAX_SESSION_PAUSE) {
all_sessions.resize(all_sessions.size()+1);
curr_sess_idx = all_sessions.size()-1;
all_sessions[curr_sess_idx].ip = ip_addr;
it->second.sess_idx = curr_sess_idx; // overwrite
it->second.last_time = sec;
} else { // continue old
curr_sess_idx = it->second.sess_idx;
it->second.last_time = sec;
}
} else { // create new session
all_sessions.resize(all_sessions.size()+1);
curr_sess_idx = all_sessions.size()-1;
all_sessions[curr_sess_idx].ip = ip_addr;
open_sessions
.insert(pair<string, OpenSessionData>
(ip_addr,
OpenSessionData(curr_sess_idx, sec)));
}
all_sessions[curr_sess_idx].urls.push_back(url);
all_reqs.push_back(
IP_URL_pair(curr_sess_idx,
all_sessions[curr_sess_idx].urls.size()-1));
}
stable_sort(all_reqs.begin(), all_reqs.end(),
HistoryComparator(all_sessions));
AllReqs::const_iterator same_url_p;
for (AllReqs::const_iterator p = all_reqs.begin(); p!=all_reqs.end();
p=same_url_p) {
int new_pattern = 1;
const string &curr_URL = all_sessions[p->sess_idx].urls[p->url_idx];
for (same_url_p = p+1;
same_url_p!=all_reqs.end()
&& all_sessions[same_url_p->
sess_idx].urls[same_url_p->url_idx]
== curr_URL;
++same_url_p) {
// compare history. Only history larger then 3
const int first_size = all_sessions[p->sess_idx].urls.size(),
sec_size = all_sessions[same_url_p->
sess_idx].urls.size();
int i;
for (i=0; p->url_idx+i < first_size
&& same_url_p->url_idx+i < sec_size
&& i < HISTORY_SIZE;
i++) {
if (all_sessions[p->sess_idx].urls[p->url_idx+i] !=
all_sessions[same_url_p->
sess_idx].urls[same_url_p->url_idx+i])
break;
}
if (i == HISTORY_SIZE) {
if (new_pattern) {
new_pattern = 0;
usage_patts.push_back(
PatternData(all_sessions[p->sess_idx].ip));
curr_patt_index = usage_patts.size()-1;
for (int j=0; j < HISTORY_SIZE; j++)
usage_patts[
curr_patt_index].urls.push_back(
all_sessions[p->
sess_idx].
urls.at(p->url_idx+j));
}
usage_patts.at(curr_patt_index).ips.push_back(
all_sessions[same_url_p->sess_idx].ip);
}
}
}
// for_each(all_reqs.begin(), all_reqs.end(), Printer<IP_URL_pair>());
for_each(usage_patts.begin(), usage_patts.end(), Printer<PatternData>());
return 0;
}
//
Видно, что никакой особой специфики C++ тут нет, исключая несколько
моментов (они будут рассмотрены подробнее чуть ниже).
- Многим стандартным функциям для осмысленной работы нужна
функция-параметр. Замыканий в языке нет, значит использование функций
автоматически приводит к загрязнению глобального (для кого-то)
пространства имён
данными и проблему всё равно не решает. Решение здесь состоит в
создании функтора (объекта-функции), т.е. объекта класса,
реализующего оператор
()
.
Особенно интересные структуры возникают, когда этот класс является
параметризованным (т.е. шаблоном).
- STL по загадочным причинам не включает хеш-таблиц (там, впрочем, есть
ассоциативные массивы, использующие деревья, см. map), этим и
объясняются приключения
<hash_map>
.
STL состоит из следующих 2-х классических частей:
- Обобщённые структуры данных.
- Обобщённые алгоритмы, включая сюда как собственно готовые алгоритмы
(двоичный поиск), так и средства их организации (итераторы).
Обобщённые они в том смысле, что написаны в общем виде, без привязки к
конкретным типам.
Как всегда, хитрости возникают, когда начинается реальная жизнь.
К примеру, если
для объектов, помещённых в контейнер, деструктор вызывается, то для
указателей -- нет.
Более глубоко осознать проблему поможет следующий пример.
#include <iostream>
#include <vector>
struct MyS {
~MyS() {
std::cout << "dtor call\n";
}
};
int main(int, char *[]) {
std::vector<MyS> v(3);
std::cout << "--------------------\n";
v.resize(4);
std::cout << "--------------------\n";
}
Если это скомпилировать gcc 3.0.2, то получим
dtor call
--------------------
dtor call
dtor call
dtor call
dtor call
--------------------
dtor call
dtor call
dtor call
dtor call
А результат работы Intel C++ Compiler версии 5.0.1 (Build 010730D0) выдаст
нечто иное:
dtor call
--------------------
dtor call
dtor call
dtor call
dtor call
dtor call
--------------------
dtor call
dtor call
dtor call
dtor call
На пример, впрочем, можно посмотреть и по-другому. Рассмотрим результат
проектирования ``без задней мысли'', когда объект типа File открывает
файл в конструкторе, а закрывает в деструкторе (см, напр., [3],
14.4). Нетрудно догадаться,
насколько увлекательным окажется хранение таких штук в std::vector.
Необходимо понимать, что деструкторы приводятся здесь лишь как пример:
``нетривиальный'' оператор присваивания тоже может вызвать ``интересные''
результаты. Мораль здесь простая -- в контейнерах STL место ``легковесным''
объектам. На этом фоне загадочно смотрится совет 8 из [5]
насчёт auto_ptr
и контейнеров: как мы видим, довольно много
чему ещё в контейнерах не место3.
Итак, перед нами мощный высокоуровневый язык программирования,
реализованный с активным использованием макропроцессора. Уже очень давно
известно, с чем столкнутся пользователи такой системы: с разнообразными
проблемами в run-time, к примеру, проблемами с производительностью.
Закончим этот подраздел 3-мя советами:
- Используйте std::vector, а не массивы, встроенные в язык.
- Используйте std::string, а не удивительные char*
строки.
- Относитесь к ручному написанию сортировки, двоичного
поиска и пр. как к табулированию квадратного корня: это уместно столь
же часто. Собственно говоря, не зря qsort и bsearch есть даже в C.
Осмысленен ещё один взгляд на STL -- как на источник идей по написанию
generic обработчиков. Под таким углом зрения стоит посмотреть, скажем, на
то, как STL удаётся вызывать деструкторы для объектов и не вызывать
для указателей. Удаётся, заметим, без использования RTTI (что естественно),
и даже на VC6.
Хотелось бы напомнить, что некоторые манипуляции с контейнерами
делают (некоторые) итераторы недействительными. Пример может
быть, например, таким:
int n = 1;
std::vector<int> v(4);
for (std::vector<int>::iterator i=v.begin(); i!=v.end(); ++i)
*i = ++n;
for (std::vector<int>::iterator i=v.begin(); i!=v.end(); ++i)
if (*i % 2)
v.erase(i);
В данном случае всё ломается4 потому, что erase вектора
делает недействительным как итератор, по которому удаляют,
так и все, большие его.
Прямолинейное решение здесь выглядит примерно так:
for (std::vector<int>::iterator i=v.begin(); i!=v.end(); )
if (*i % 2)
i = v.erase(i);
else
++i;
Безусловно, erase-remove из [5], совет 32 выглядит
поприятнее, особенно с использованием boost::lambda, но мораль,
собственно, в другом. Угадайте, что будет с итераторами вектора,
если в вектор что-то добавить? А если это был список?
По ходу дела заметим, что i++ -- это совсем не ++i,
и в С++ -- особенно. Как известно, постинкремент вернёт старое
значение, а преинкремент -- новое. Это не очень сильно влияет
на производительность для всяких целых и пр., но для полноценных
типов разница может быть существенной: старое значение надо где-то
сохранить.
Каждому ясно, что функция сортировки окажется мало кому полезной, если
не будет уметь использовать произвольный оператор сравнения. Просто
указатель на функцию здесь подходит не всегда, т.к. иногда нужно
иметь данные, локальные именно для этого сравнивателя. Проблема
становится ещё более прозрачной, если вместо sort рассматривать
какой-нибудь for_each.
Решением являются функторы -- это просто классы с перегруженным
operator()
. Благодаря перегрузке, функтор можно использовать
там, где нужен вызов функции, но на самом деле это объекты, и потому
у них есть состояние (а не только поведение). Необходимо
быть готовым к тому, что функторы передаются по значению.
Примерно вот так выглядит Универсальный Искатель Максимумов.
#include <iostream>
#include <vector>
#include <algorithm>
template <typename T> class Find_Max {
bool init;
T curr_max;
public:
Find_Max() : init(0) {}
T operator() (const T &a) {
if (! init) {
init = 1;
curr_max = a;
} else
if (curr_max < a) curr_max = a;
return curr_max;
}
T get_max() const {
return curr_max;
}
};
std::vector<int> v;
std::cout << std::for_each(v.begin(), v.end(), Find_Max<int>()).get_max() ;
К сожалению, использовать accumulate (казалось бы, естественное
желание) для этой задачи удастся, лишь передавая ему INT_MIN
, что, на
мой вкус, пионерство5.
Рассмотрем, например, поиск в контейнере первого вхождения элемента,
удовлетволяющего некому критерию.
#include <iostream>
#include <vector>
#include <algorithm>
bool div_3(int a) {
return (a % 3) == 0;
}
int main(int, char *[]) {
std::vector<int> v;
for (int i=1; i<20; i++) v.push_back(i);
std::cout << *( find_if(v.begin(), v.end(), div_3) );
return 0;
}
Всё прямолинейно, пока мы не захотим искать первый не
удовлетворяющий. Пытливые слушатели знают, естественно,
про not1, но прямолинейная попытка написать
#include <functional>
std::cout << *( find_if(v.begin(), v.end(), not1(div_3)) );
приведёт лишь к возмущению компилятора:
find_if.cpp:16: no matching function for call to `not1(bool (&)(int))'
Не слишком понятно, но можно догадаться, что not1 ждёт чего-то
иного. Решение здесь такое:
#include <functional>
std::cout << *( find_if(v.begin(), v.end(),
std::not1(std::ptr_fun(div_3))) );
Примерно из той же оперы mem_fun, применяемый, когда
нужно выдать указатель на метод за функцию.
Адаптируемость функторов обеспечивает наследование их от
экземпляров шаблонов unary_function
, binary_function
,
инстанцированных соответствующим образом. Среди полезных вещей,
которые это средает возможным (компилируемым) -- bind1st,
которая связывает 1-ый аргумент 2-аргументной функции, в результате
чего получается 1-аргументная функция.
list<int> L;
...
list<int>::iterator first_nonzero =
find_if(L.begin(), L.end(), bind1st(not_equal_to<int>(), 0));
Если при сравнении Перловских for(;;) {}
и for $l (@l) {}
всё становится ясно очень быстро, то выбор между явным выписыванием
итераторов и for_each(l.begin(), l.end(), pp(17))
выбор
не всегда очевиден. Проблема тут в том, что в языке отсутствуют
анонимные функции, и, значит,
читатель должен читать текст не подряд, что резко снижает удовольствие
от алгоритмов STL.
С другой стороны, простота функций, включённых в STL, не должна
обманывать: известны печальные результаты эксперимента Бентли,
заставлявшего профессиональных программистов писать двоичный поиск,
который никто не написал.
Рассмотрм пример.
#include <iostream>
#include <vector>
#include <numeric>
template <typename T> struct Summer {
T operator () (const T &a, const T &b) const {
return a+b;
}
};
int main(int, char *[]) {
std::vector<float> v;
for (int i=5; i>1; i--) v.push_back(i+0.1);
std::cout << std::accumulate(v.begin(), v.end(), 0., Summer<float>());
return 0;
}
6Чем это лучше явного цикла? Всем. Только хуже, а не лучше: читая исходник,
я должен прыгать куда-то в сторону. Ужас!
Как не удивительно, решение проблемы существует в рамках библиотеки,
без изменения языка. ЭТО называется boost::lambda.
#include <iostream>
#include <vector>
#include <numeric>
#include "boost/lambda/lambda.hpp"
using namespace boost::lambda;
int main(int, char *[]) {
std::vector<float> v;
for (int i=5; i>1; i--) v.push_back(i+0.1);
std::cout << std::accumulate(v.begin(), v.end(), 0., _1 + _2);
return 0;
}
Каждому ясно, что так оно гораздо веселее. Основная проблема тут
в том, что, спустя 5 лет после выхода Стандарта, в реальной жизни
остаётся куча компиляторов, boost::lambda скомпилировать неспособных.
Впрочем, народные сказители утверждают, что перед разработчиками
VC7.1 прямо так и была поставлена задача: ``Чтобы Boots компилировался!''.
В любом случае, текущая реальность ставит перед нами вопрос:
а стоит ли таскать с собой кусок весьма неочевидных7 текстов ради локальных и чисто синтаксических
преимуществ?
Как всегда, интересные проблемы возникают, когда что-то начинает ломаться
и взрываться. В данном случае это -- то, что в контейнеры складываем.
Истоки проблемы понятны: если обычная библиотека выбрасывает исключение,
то сама же она его и генерирует, а вот ситуация с STL не такая: во всех
нетривиальных случаях STL вызывает пользовательский код. Здесь возникает
конфликт интересов: с одной стороны, если во время выполнения функции
вставки в контейнер пользователький оператор сравнения сгенерировал
исключение, идеальным было бы, чтобы контейнер откатился до состояния
перед началом вставки, с другой, если никаких исключений не произошло,
пользователь вовсе не хочет платить замедлением и памятью за защиту
восстановление после исключений, которых он вовсе, возможно, и не
генерирует.
Ясно, что требования противоречивые, и в качестве компромисса предложено,
кратко говоря, вот что: для всех операций выполняется ``базовая гарантия''
(нет утечек памяти, после исключения контейнер остаётся в целостном
состоянии, хотя и не известно, в каком именно и пр.), а для некоторых
ключевых (напр., push_back
) -- ``сильная'', т.е. либо операция
полносью выполняется, либо откатывается.
Ничего, естественно, не гарантируется, если пользователь библиотеки
ведёт себя антиобщественно, т.е. выбрасывает исключения из деструкторов
и пр.
Более подробно про это см. [3], прил. Д, Герб Саттер Решение
сложных задач на С++.
- Посмотреть на logparser.cpp через профилировщик и ускорить
его (не профилировщик).
- Переписывание на С++ кусков чужой С-шной программы.
"Resource acquisition is initialization". [
3]
Если стремиться давать определения и не стремиться, чтобы они что-то
определяли, по СП можно понимать ``легковесные'' объекты, которые похожи
на указатели тем, что определяют операторы *
и ->
, но
при этом ведут себя более интеллектуально, чем указатели обычные. Главная
область интеллектуализации -- более управлямая семантика владения:
действительно, крайне просто скопировать указатель ``на сторону'', и
крайне сложно потом решить, кто может разрушать указываемый объект и когда
он это может.
Для полноты ощущений обсудим совершенно локальную проблему -- возврат
``обычных'' указателей из функции. Ну, действительно, что может быть
проще:
class Foo;
Foo *func() {
Foo *f = new Foo(113);
...
return foo;
}
Но в C, как известно, получать возвращаемое значение необязательно, и
транслятор не осудит написавшего
func();
В результате гарантирована утечка памяти, и, возможно, кое-что поинтереснее.
Конечно же, в чистом виде так никто не напишет, но вот если вспомнить об
эволюционирующих годами проектах... Не стоит, видимо, сомневаться, что
в больших программах таких тонкостей немало. Вопрос: а почему не
написать просто
class Foo;
Foo func() {
Foo foo(113);
...
return foo;
}
Ответ: а вот чтобы таких мыслей не возникало, и было придумано задание
на предидущей лекции.
Боюсь, что каждый из нас писал нечто наподобие:
class Foo {
Bar* m_bar;
public:
Foo() {
m_bar=new Bar;
...
}
~Foo() {
delete m_bar;
}
}
Проблема с этим кодом в том, например, как он отреагирует на исключение,
выброшенное вместо точек. Здесь могут быть несколько решений, например:
- Никаих исключений здесь не выбрасывать.
- Все их ловить.
Ясно, что всё это можно, но отвратительно.
Общий вывод из предидущего пункта такой: руками можно сделать всё,
но вы этого не хотите. Можно требовать от программистов внимательного
чтения исходников, можно вкладывать ресурсы в тестирование, но гораздо
конструктивнее сделать так, чтобы проблемы решались техническими
механизмами. Как и во многих других программистских областях,
проблему решает ``принцип Дейкстры'': ``не работает -- введите ещё
один уровень косвенности''. СП этот уровень и есть.
В наипростейшем варианте generic СП -- это вот что такое:
template <class T> class SmartPtr {
T* pointee_; ...
public:
explicit SmartPtr(T* pointee) : pointee_(pointee);
SmartPtr& operator=(const SmartPtr& other);
~SmartPtr();
T& operator*() const { ... return *pointee_; }
T* operator->() const { ... return pointee_; }
};
Естественно, если начать придираться к этому коду, клочки по закоулочкам
полетят моментально8. Впрочем,
идея ``легковесной оболочки'' выражена здесь совершенно понятно.
Попытавшись осознать в терминах обобщённого программирования, чего же
делается, можно понять, что на самом деле за T скрываются 3 совершенно
разных типа:
- Хранимый тип.
- Тип, возвращаемый
operator*()
.
- Тип, возвращаемый
operator->()
.
Перебирая осмысленные варианты, можно выделить следующие стратегии
управления владением:
- Глубокое копирование. При копировании СП происходит копирование
того, на что он указывает. Понятно, что подобное поведение может быть
достигнуто и без СП, перегрузкой оператора присваивания и пр.
- COW9. К сожалению,
operator->()
нельзя
перегрузить с учётом константности, потому только через канонические СП
COW не реализуется. Ничто, естественно, не мешает использовать
идеи СП, отказавшись от синтаксиса указателей. Синтаксис --
дело десятое (в разумных пределах).
- Подсчёт ссылок в той или иной форме. Может быть реализован как через
совместно используемый счётчик, так и, к примеру, хранением
владельцев в контейнере. Здесь всё понятно, включая проблемы: вспомним о
циклическом использовании.
- Исключительное владение. По нетехническим причинам эта штука стала
частью стандарта в виде
std::auto_ptr
. Механизм таков: в каждый
момент времени есть лишь один указатель на объект, при копировании указателя
старый изничтожается.
Привязать время жизни ресурса ко времени жизни контекста -- вот
наша цель.
Главные достоинства std::auto_ptr
-- простота семантики,
эффективность реализации и стандартность.
Предназначение std::auto_ptr
состоит в таком хранении указателя
на объект, чтобы указатель максимально напоминал автоматическую
переменную.
#include <iostream>
#include <memory>
class Foo {
public:
int p;
Foo(int p): p(p) {};
};
class HorrorError {
};
void func() {
throw HorrorError();
}
void f() {
Foo *f = new Foo(12);
std::auto_ptr<Foo> foo(f); // *
func();
}
int main(int, char *[]) {
try {
f();
} catch(...) { }
}
Утечек ресурсов здесь не будет, и все деструкторы будут вызваны без
утомительных try {} catch {}
на каждом уровне, где есть new.
Размещать Foo в стеке может оказаться10 неудобным по соображениям:
- размера (вспомним о стомегабайтных матрицах),
- необходимости передавать вниз по стеку.
Семантика исключительного владения может представиться
слишком ограничительной, но зато, если удалось в неё
уложиться, вопрос «Когда пора вызывать деструктор?»
решается гораздо легче. Впрочем, создатель STL в неё не
уложился: std::auto_ptr
и вообще объекты с
подобным поведением для работы с STL
непригодны11.
Используя этот механизм, необходимо проявлять бдительность.
#include <iostream>
#include <memory>
class Foo {
public:
int p;
Foo(int p): p(p) {};
};
void callee(std::auto_ptr<Foo> a) {}
int main(int, char *[]) {
std::auto_ptr<Foo> foo(new Foo(12));
callee(foo);
std::cout << foo->p; // всё самоуничтожится
}
Параметр callee передался по значению, то есть вызвался оператор
присваивания с закономерным печальным итогом для любителей использовать
foo после вызова.
На базовом уровне всё просто (используется, как нетрудно догадаться,
boost::shared_ptr).
#include <vector>
#include <list>
#include <iostream>
#include <algorithm>
#include <boost/shared_ptr.hpp>
struct Foo {
Foo( int _x ) : x(_x) {}
~Foo() { std::cout << "Destructing a Foo with x=" << x << "\n"; }
int x;
};
typedef boost::shared_ptr<Foo> FooPtr;
struct FooPtrOps {
bool operator()( const FooPtr & a, const FooPtr & b )
{ return a->x > b->x; }
void operator()( const FooPtr & a )
{ std::cout << a->x << "\n"; }
};
int main() {
std::vector<FooPtr> vect;
std::list<FooPtr> lst;
for (int i=0; i<10; i++) {
FooPtr foo_ptr( new Foo(i) );
vect.push_back( foo_ptr );
lst.push_back( foo_ptr );
}
std::sort(vect.begin(), vect.end(), FooPtrOps());
std::cout << "vect:\n";
std::for_each(vect.begin(), vect.end(), FooPtrOps() );
std::cout << "lst:\n";
std::for_each(lst.begin(), lst.end(), FooPtrOps() );
return 0;
}
Не все указатели должны быть равноправны
Если немножко подумать, станет ясно, что СП в каком-то смысле
провоцируют расползание ответственности: это теперь не просто
указатель, а указатель с мозгом, и значит, его можно раздавать направо
и налево, раз проблемы повисших указателей нет. В результате объект,
конечно, не уничтожится, пока на него есть ссылки, но иногда объекты
нужно уничтожать.
Решение проблемы состоит в том, чтобы ввести слабые ссылки,
во всём похожие на настоящие, но только не считающиеся, когда мы
считаем число использований. В рамках boost::shared_ptr)
это делается так: weak_ptr<Foo> wpf(foo_ptr)
.
Получить из weak_ptr то, что мы туда положили
можно, вызвав lock: она вернёт shared_ptr<Foo>
, если же
процесс освобождения произошёл, то 0.
Интересно, что в языках со сборкой мусора вместо умного указателя
возвращают обычно сам объект. Понятно, от чего мы в lock страхуемся:
от того, что пока мы с объектом работаем, некто может свой
shared_ptr
освободить, и тогда объект окажется в свободной памяти с понятными
результатами. В языках же со сборкой мусора этого мусорщик не допустит.
Кто не слеп, тот видит, что все описанные проблемы ни в коей мере не
специфичны для C++: любая современная система программирования
содержит нечто подобное. Некая специфика здесь лишь в том, что все
рассматриваемые вещи -- библиотеки, а не часть языка. И ещё, пожалуй,
что в C++ нет защиты памяти, и искать много сложнее.
На уровне слов с вышеизложенным легко согласиться. Но после того,
как вы попишете на языке со сборкой мусора, вы убедитесь, что там
проблемам деструкции можно уделять значительно меньше внимания.
Реальность здесь многоуровневая:
- Сборка мусора вряд ли окажется быстрее прямого управления
ресурсами, но чтобы в современных условиях почувствовать ``тормоза'',
задача должна быть уж очень специфической. Что-то, например, в стиле
``так мы влезаем в память, а со сборкой мусора -- нет, и трешинг
начинается''.
- Какой-нибудь коннект к БД -- полне себе ресурс, но автоматическое
его освобождение вообразить непросто.
- Известно (см. [4]), что в язык не ввели finally
как раз для того, чтобы стимулировать хороший стиль, выраженный
в эпиграфе.
Рассмотрим следующий модельный пример: есть некое (неизвестное заранее)
количество окон и у каждого окна есть кнопка закрытия. При закрытии
одного одного окна все окна должны закрываться. Наивная реализация
здесь состоит в том, чтобы создатель окон звонил всем уже созданным
и знакомил их с новичком.
Если чуть-чуть задуматься о схеме уничтожения окон, станет понятно,
что она никуда не годиться. При любви к навешиванию ярлыков,
ярлык здесь однозначен: ``жёсткая связанность''. И впрямь,
обработчик закрытия каждого окна должен знать все окна, которые
которые ему предстоит закрыть. Легко себе предстваить,
во что это выльется, если окон станет побольше.
Решение
здесь настолько хорошо известно, что даже названий у него
много -- это паттерн Publish-Subscribe12. Говоря простыми словами, вместо унылого
явного уничтожения нужно написать 2 вещи:
- регистратора, куда будут звонить все заинтересованные
в событиях и говорить: ``Если ЭТО случится, сообщите нам''.
- оповещателя, который, когда ЭТО случается, обзванивает всех.
Видно, что, кроме возможности просто узнать,
кто заинтересован в событиях и отсутствия жёсткой заданности
заинтересованных, мы получили ещё важную возможность --
возможность удалять и добавлять слушателей событий на лету,
в динамике.
Что тут не столь очевидно и весьма важно: перейдя к подписке,
мы снизили связанность классов, и у этого кроме плюсов есть
и минусы: доля динамики возросла, потому статически (читая
текст) разобраться, что происходит, стало труднее. Это
довольно важный пункт.
Типичная проблема, возникающая при использовании
паттерна Publisher -- утечки памяти. Проблема возникает вот где:
всё замечательно, пока объект договаривается слушать
оповещения о разных событиях, но что делать, когда этот
объект более не нужен? Сборщик мусора его не освободит, пока
на него есть ссылки, а ссылки мы щедро раздавали всем,
к кому записывались в слушатели. Конечно, можно им позвонить
и попросить, чтобы про нас забыли,
но для этого, по крайней мере, нужно знать всех, к кому мы
записались. В результате возрастает связанность, от которой мы уходили,
внедряя Observer-ов (к примеру, память из под Observable не освободится,
пока на него есть ссылки, а ссылок у нас -- в каждом Наблюдателе).
Видно, что проблема, в общем, решаемая, но на пути есть
масса мелких хитростей.
Альтернативой ручному манипулированию может служить использование слабых
(weak) ссылок, см. 2.4.3. Идея
их использования в следующем. Вместо того, чтобы (скажем) хранить
в Observer-е указатель-на-окно, мы поместим туда
weak_ptr<Окно> слабый-указатель-на-окно(указатель-на-окно),
и теперь никаким явным
удалением можно не озадачиваться. Естественно, что если от
окна не останется никакого следа, кроме слабых ссылок в Наблюдателях,
то ничего работать не будет, но кропотливой работы всё равно стало
меньше.
Пункт носит необязательный характер.
Рассмотрим следующий патологический код. Ясно, что никто в здравом уме
так писать не будет, но вот что напечатается и почему именно k?
#include <iostream>
struct Qq {
int k;
};
struct Baz {
int j;
Baz(int j) : j(j) {}
const Qq *operator->() const {
std::cout << "const Qq *operator->() const\n";
return 0;
}
};
struct Bar {
Baz i;
Bar(int i) : i(i) {}
const Baz operator->() const {
std::cout << "const Baz operator->() const\n";
return i;
}
};
struct Foo {
Bar b;
Foo(Bar b) : b(b) { }
const Bar operator->() const {
std::cout << "const Bar* operator->() const\n";
return b;
}
};
int main(int, char *[]) {
Foo f(10);
int ii = f->k;
return 0;
}
Оказывается, напечатается
const Bar* operator->() const
const Baz operator->() const
const Qq *operator->() const
, а никаких i и j тут не видно! Как известно,
результатом operator->()
может быть либо указатель, либо то, к
чему можно применить operator->()
. Так вот, разгадка проста: при
виде ->
транслятор проявляет упорство и не успокаивается, пока может
применять operator->()
(что и видно по печатям), после же этого
доступно лишь k.
Раздел первоначально написан в 2002-м году Олегом
Александровичем Пыхтеевым.
Шаблоны представляют собой вполне современное языковое средство,
но они ограничены временем компиляции. Семантически нетривиальная
часть RTTI относится ко времени исполнения, но в C++ она гораздо
более слабая, чем в современных языках.
Изначально коренным отличием языка C++ от языка C было наличие в
первом классов. Следствие этого -- более общее понятие типа в C++. А
раз понятие типа несколько изменилось, то и способы работы с типами
должны были рано или поздно эволюционировать. Главным образом это
касается приведений типов. Если приведение между простыми типами,
которые унаследованы от C, не вызывает больших трудностей в понимании,
то приведение между пользовательскими классами в ряде случаев не так
очевидно и требует хорошего понимания сути происходящего. Если верить
Страуструпу (а если не верить ему, то кому тогда вообще верить?), то
некорректное приведение типов -- один из основных источников ошибок в
C++.
Одним из недостатков приведения типов в стиле C является то, что в C
приведение типов всегда происходит статически, то есть код,
непосредственно выполняющий приведение, генерируется на этапе
компиляции. При использовании C это не является большой проблемой, потому
что типы языка C таковы, что компилятор обладает достаточной
информацией, чтобы корректно сгенерировать этот код заранее или
сообщить о невозможности приведения. В C++ это не так. Рассмотрим
следующий пример. Пусть наша система предоставляет информацию о
продуктах и компаниях. Например, это может быть справочная система или
электронный магазин. Каждая компания и продукт имеют свой
идентификатор. Объектная модель может быть такой:
Файл objects.h:
// Базовый класс для информационных объектов
// Интересен лишь как базовый класс, сам по себе ничего делает
class InfoObject
{
protected:
long id; // Идентификатор
public:
// Получить идентификатор информационного объекта
long getID() const {return id;}
// Выполняет загрузку объекта содержательной информацией (напр., из файла)
virtual void load(){}
InfoObject(long id) : id(id){} // Конструктор
virtual ~InfoObject(){} // Виртуальный деструктор
};
// Класс продукта
class Product : public InfoObject
{
public:
// Выполняет загрузку объекта продукта содержательной информацией
void load();
~Product(){} // Деструктор объекта продукта
Product(long id) : InfoObject(id){} // Конструктор объекта продукта
// Другие атрибуты продукта
long price; // Цена продукта
};
// Класс компании
class Company : public InfoObject
{
public:
// Выполняет загрузку объекта компании содержательной информацией
void load();
~Company(){} // Деструктор объекта компании
Company(long id) : InfoObject(id){} // Конструктор объекта компании
// Другие атрибуты компании
long yearEstablished; // Год основания компании
};
Содержательное наполнение компании и продукта информацией
осуществляется виртуальным методом load():
Файл objects.cpp:
#include "objects.h"
// Выполняет работу по наполнению объекта компании информацией
// Здесь может быть загрузка информации из файла или из SQL базы
// В данном случае просто заглушка, дабы не усложнять без надобности
void Company::load()
{
yearEstablished=1999;
}
// Выполняет работу по наполнению объекта продукта информацией
// Здесь может быть загрузка информации из файла или из SQL базы
// В данном случае просто заглушка, дабы не усложнять без надобности
void Product::load()
{
price=1000;
}
Загрузка компании или продукта может быть довольно дорогостоящей
операцией, поэтому целесообразно кэшировать информационные объекты.
При этом вместимость кэша естественно должна быть ограничена,
поскольку все информационные объекты могут в память и не влезть. То
есть объекты должны при необходимости выталкиваться. Кэширование может
быть реализовано, например, так:
Файл InfoObjectsCache.h:
#include <map>
#include "objects.h"
// Ассоциативный массив, сопоставляющий идентификатору объекта сам объект
typedef std::map<long, InfoObject *> ObjectsMap;
// Менеджер информационных объектов
class InfoObjectsCache {
long maxSize; // Максимальное количество памяти, занимаемое объектами
long realSize; // Реально занимаемое количество памяти
// Ассоциативный массив, сопоставляющий идентификатору объекта сам объект
ObjectsMap map;
void cleanCache(); // Выбрасывает из кэша один объект
public:
// Конструктор менеджера
InfoObjectsCache(long maxSize):maxSize(maxSize),realSize(0),map(){}
void Put(InfoObject *); // Поместить указанный объект в кэш
InfoObject* Get(const long);// Возвращает объект из кэша по идентификатору
};
Файл InfoObjectsCache.cpp:
#include "InfoObjectsCache.h"
// Возвращает объект из кэша
InfoObject* InfoObjectsCache::Get(const long id)
{
// Проверить есть ли объект в кэше
// И если есть, то вернуть его
if (map[id]) return map[id];
// Если объекта в кэше нет, вернуть NULL
return NULL;
}
// Поместить объект в кэш
void InfoObjectsCache::Put(InfoObject* pObject)
{
// Посчитать размер объекта
long size = sizeof(*pObject);
// Если размер объекта больше чем максимально допустимый, не кэшировать объект
if ( size > maxSize) return;
// Взять идентификтор
long id = pObject->getID();
// Если объект с таким же идентификатором уже есть в кэше, удалить его
if (map[id]) map.erase(id);
// Пока объект не влезает в кэш, выкидывать оттуда другие объекты
while (realSize + size > maxSize)
{
cleanCache();
}
// Кэшировать объект
map[id] = pObject;
realSize += size;
}
// Убрать из кэша первый объект
void InfoObjectsCache::cleanCache()
{
// Взять первую пару ключ-значение
ObjectsMap::iterator it = map.begin();
// Взять значение
InfoObject *pObject = it->second;
// Пересчитать размер кэша
realSize -= sizeof(*pObject);
// Убрать из кэша
map.erase(it);
}
А использовать это можно так:
Файл client.cpp:
#include <iostream>
#include "InfoObjectsCache.h"
using namespace std;
int main(int argc, char **argv)
{
// Построить кэш
InfoObjectsCache cache(10000);
// Сконструировать объект продукта
Product* pProduct = new Product(100);
pProduct->load();
// Закэшировать продукт
cache.Put(pProduct);
// Сконструировать объект компании
Company* pCompany = new Company(200);
pCompany->load();
// Закэшировать компанию
cache.Put(pCompany);
// Достаем объекты из кэша
if (pProduct = (Product*)cache.Get(100))
// Все правильно, достали продукт
cout << "Product price is " << pProduct->price << endl;
else
cout << "Failed to get product!" << endl;
if (pCompany = (Company*)cache.Get(100))
// Ошибка!!! По идентификатору 100 реально расположен продукт,
// а мы сделали из него компанию
cout << "Company established in " << pCompany->yearEstablished << endl;
else
cout << "Failed to get company!" << endl;
return 0;
}
Проблема здесь заключается в том, что когда мы достаем из кэша
информационный объект, то не знаем, является ли он на самом деле
объектом продукта или объектом компании. На этапе компиляции нет
никакой возможности узнать тип объекта, который будет возвращен по
идентификатору 100. Тем более если учесть, что этот тип может меняться
во время работы программы. Никакой ошибки во время выполнения и уж тем
более во время компиляции не произойдет. Код, сгенерированный
компилятором формально и невзирая не на что приведет объект
InfoObject* к типу Company* так как если бы это действительно был бы
объект типа Company*. В результате напечатается ерунда и никто никак и
никогда не узнает, что это ерунда. В данном случае напечатается:
Product price is 1000
Company established in 1000
Для того чтобы корректно обрабатывать такие ситуации необходимо
обладать информацией о типе объекта во время выполнения и обладать
инструментами, с помощью которых можно этой информацией
пользоваться. Такая поддержка получила название RTTI -- Run-Time Type
Information. По существу, RTTI предоставляет два оператора --
dynamic_cast
и typeid
. Ситуацию, описанную выше, можно было бы
исправить так:
if (pCompany = dynamic_cast<Company*>(cache.Get(100)))
// Если попали сюда, значит, приведение прошло корректно
cout << "Company established in " << pCompany->yearEstablished << endl;
else
cout << "Failed to get company!" << endl;
В результате будет напечатано:
Product price is 1000
Failed to get company!
Некоторые специалисты считают, что механизм RTTI избыточен и всегда,
если подумать, можно обойтись без него. Отсюда вопрос к читателю: Как
можно в примере выше обойти использование RTTI? И еще один
вопрос. Кэш, реализованный так, как это сделано в примере, на практике
применять нельзя. Почему?
Оператор принимает два аргумента, тип, заключенный в угловые скобки и
указатель или ссылку, заключенную в круглые скобки. В случае с
указателем синтаксис такой:
T* dynamic_cast<T*>(p)
Если указатель p можно корректно привести к типу T*, то оператор
выполнит это приведение и вернет указатель типа T*. В противном
случае вернется NULL. Корректность подразумевает выполнение следующих
условий:
- Указатель p не NULL
- Объект по адресу p содержит информацию о своем типе (об этом ниже).
- Объект по адресу p действительно содержит подобъект типа T.
Приведение не нарушает правил доступа. То есть не производится
несанкционированная попытка привести к базовому классу, который
унаследован приватно (class B : private A) или ограничено (class B :
protected A). Как правило, это можно определить еще на этапе
компиляции. Объект по адресу p содержит не более чем один подобъект
типа T, то есть не возникает неоднозначности.
Последний случай может возникнуть как при upcast'е (то есть при
приведении от наследника к предку), что вполне очевидно, учитывая
множественное наследование, так и при downcast'е (то есть при
приведении от базового к потомку), что тоже понятно, если вспомнить
про виртуальное наследование.
Динамическое приведение ссылок имеет такой синтаксис:
T& dynamic_cast<T&>(r)
Поведение в этом случае имеет особенность -- ссылке нельзя присвоить
NULL, если приведение не удалось. Поэтому в этом случае кидается
исключение bad_cast
. Таким образом, если динамическое приведение
указателей, как правило, оформляется в стиле
if (pT = dynamic_cast<T*>(pS))
{
// Удалось привести
}
else
{
// Не удалось привести
}
то динамическое приведение ссылок -- в стиле
try
{
rT = dynamic_cast<T&>(rS);
// Удалось привести
}
catch (bad_cast)
{
// Не удалось привести
}
Итак, использование RTTI позволяет получать информацию о типе объекта
во время исполнения. Это значит, что эта служебная информация должна
быть где-то размещена. Простым C'шным типам эта информация, как и
поддержка RTTI не нужна. Даже наоборот, я хочу, чтобы double у меня
всегда занимал 8 байт, а char -- 1 байт. Если это вдруг перестанет быть
правдой, то огромное количество программ накроется медным тазом.
Таким образом, все объекты разделяются на две категории:
- Те, с которыми ассоциирована информация о типе, так что их тип можно
определить во время исполнения независимо от контекста;
- Объекты, для которых это утверждение неверно.
Критерий здесь такой: Если объект полиморфного типа (то есть имеет
виртуальные функции), то он относится к первой категории, иначе -- ко
второй. Выбор именно такого критерия связан с особенностями размещения
в памяти полиморфных и не полиморфных объектов. Грубо говоря, у
полиморфного объекта есть таблица виртуальных функций, куда можно
засунуть ссылку на информацию о типе, а у не полиморфных объектов эту
информацию спрятать некуда. Таким образом, все старые добрые C'шные
типы и простые классы продолжают работать также как и раньше к
всеобщему облегчению. С другой стороны, если вы планируете делать
динамическое приведение для типа, позаботьтесь снабдить его хотя бы
одной виртуальной функцией (например, виртуальным деструктором, что
полезно также и по другим соображениям).
Статическое (то есть обычное) приведение типов имеет одно ограничение,
которое может показаться странным: компилятор не может сгенерировать
код для приведения от базового типа к потомку, если наследование
виртуальное. Это связано с тем, что подобъект виртуального базового
класса внутри объекта класса потомка не может вычислить адрес
объемлющего объекта на этапе компиляции. Зато этот адрес может быть
вычислен во время исполнения и, следовательно, такое приведение может
быть выполнено оператором dynamic_cast
. Например:
// Полиморфный базовый класс
class B { /* ... */ virtual void f(){} };
// Полиморфный базовый класс, который наследуется виртуально (см ниже)
class V { /* ... */ virtual void g(){} };
// Неполиморфный базовый класс, который наследуется виртуально (см ниже)
class X { /* нет виртуальных функций */ };
class D : public B, public virtual V, public virtual X
{
// ...
};
int main(int argc, char **argv)
{
D d;
B* pb = &d;
// Правильно. Корректность не проверяется.
D* p1 = (D*)pb;
// Правильно. Проверется в run-time
D* p2 = dynamic_cast<D*>(pb);
V* pv = &d;
// Ошибка: приведение из базового виртуального класса
// Недостаточно информации для приведения.
D* p3 = (D*)pv;
// Правильно. Выполняется в run-time
// Информации достаточно.
D* p4 = dynamic_cast<D*>(pv);
X* px = &d;
// Ошибка: приведение из базового виртуального класса
// Недостаточно информации для приведения.
D* p5 = (D*)px;
// Ошибка: приведение из неполиморфного базового класса
// По указателю px нет информации о типе
D* p6 = dynamic_cast<D*>(px);
return 0;
}
Большинство нужд связанных с добыванием информации о типе во время
выполнения покрывается оператором dynamic_cast
. Однако иногда хочется
не просто корректно приводить классы или интересоваться может ли класс
быть приведен к указанному, но и:
- узнать точный тип, к которому принадлежит объект (оператор
dynamic_cast
может сказать только о приводимости к типу);
- узнать что-нибудь точный тип объекта для получения дополнительной
информации о нем. Например, в целях отладки может быть полезным
печатать имя класса злосчастного объекта.
Для этого служит оператор typeid, имеющий сигнатуры:
const type_info& typeid(type_name) throw(bad_typeid);
const type_info& typeid(expression);
Он принимает в качестве аргумента имя типа или выражение и возвращает
ссылку на объект класса type_info
, который этот тип описывает. Класс
type_info
определен в заголовочном файле <type_info.h>
. Точное
определение этого класса зависит от реализации, но это полиморфный
тип, в котором имеются операторы сравнения и операция, возвращающая
имя типа:
class type_info
{
// Представление зависит от реализации
private:
// Пользователи не могут копировать type_info
type_info(const type_info&);
// И присваивать не могут
type_info& operator=(const type_info&);
public:
~virtual ~type_info(); // Класс полиморфный
int operator==(const type_info&) const; // Можно сравнивать
int operator!=(const type_info&) const;
int before(const type_info&) const; // Упорядоченный
const char* name() const; // Имя типа
};
Дабы пользователи не копировали объекты типа type_info
, конструктор
копирования и оператор присваивания объявлены приватными, Операторы
сравнения перегружены не для красоты -- нет никакой гарантии, что,
выполняя typeid
на объекты одного и того же типа, вы будете получать
ссылки на один и тот же объект type_info
. Хотя иногда это может быть
действительно так. Функция before()
предназначена для сортировки
объектов типа type_info
, чтобы их можно было хранить в хэш-таблицах и
т.п. Отношение, описываемое данной функцией, никак не связано с
отношением наследования и, более того, может давать разные результаты
от запуска к запуску. В этом смысле before()
напоминает оператор
взятия адреса. Оператор typeid
можно использовать, например, так:
if (pT = dynamic_cast<T*>(pS))
{
// Сделать что-нибудь полезное
}
else
{
// Печать отладочной информации
cout << "Failed to cast object of type " << typeid(*pS).name() << endl;
}
Хотя сравнение типов и работает корректно, все же рекомендуется им не
злоупотреблять. Так, например, советуют избегать следующих
конструкций:
void rotate(Shape &r)
{
if (typeid(r)==typeid(Circle))
{
// Ничего не делать
}
else if (typeid(r)==typeid(Triangle))
{
// Повернуть треугольник
}
else if (typeid(r)==typeid(Square))
{
// Повернуть квадрат
}
// ...
}
Хотя эта конструкция будет работать, писать так не надо. Такой код не
будет корректно обрабатывать классы, производные от встречающихся в
нем. Следовательно, при добавлении в программу нового класса, его
придется модифицировать. Возможно, следующий код, использующий
виртуальные функции, будет более хорошим решением:
class IRotable
{
public:
virtual rotate()=0;
};
void rotate(Shape &r)
{
if (IRotable *p = dynamic_cast<IRotable*>(*r))
{
p->rotate();
}
}
Наверное, основной проблемой с приведениями типов в стиле C является
очень расплывчатая трактовка этого самого приведения. Видя перед собой
оторванное от контекста выражение с приведением типа, пользователь не
в состоянии определить, что имел в виду автор. Например:
const X* pX = new X;
// ...
pY = (Y*) pX;
Хотел ли программист получить указатель на тип никак не связанный с X?
Или убрать атрибут const? Или и то и другое одновременно? Может быть,
он намеревался получить доступ к классу Y, базовому для X? Синтаксис
один и тот же, а семантика может быть разной, в зависимости от
контекста.
Другая опасность кроется в молчаливой бесконтрольности
приведения. Вследствие этого, могут возникать ситуации, когда
безобидное, на первый взгляд, изменение способно коренным образом
поменять смысл выражения, например:
class X : public A, public B { /* ... */ };
void f(X *pX);
{
((B*)pX)->g(); // Вызывается g из класса B
}
Изменим определение класса X так, чтобы B больше не являлся для него
базовым. Смысл выражения (B*)pX станет совершенно другим, а компилятор
даже не сможет обнаружить ошибку. Используя синтаксис C для
приведений, я не могу явно указать, что я хочу от этого приведения
добиться. Как следствие и компилятор в случае изменений не может мне
сказать, что я хотел не этого -- синтаксис-то такой же. Эти причины
привели к возникновению новых операторов статического приведения
типов, которые призваны распределить функциональность старых по разным
категориям:
static_cast<T>(e) // "хорошие" приведения типов,
// в основном для навигации по иерархии классов
reinterpret_cast<T>(e) // опасные приведения
// для безопасного использования результат такого
// приведения должен быть приведен к исходному типу
const_cast<T>(e) // отбрасывание модификатора const
Довольно трудно строго коротко и точно описать в каких же случаях
следует применять нотацию static_cast
-- здесь есть ряд
тонкостей. Грубо говоря, static_cast
предназначен для
приведения между соотносимыми типами -- между указателями различных
типов, между типом-перечислением (enum) и целым, между int и float,
между объектами, связанными отношением наследования и т.п.
В первом приближении можно считать, что static_cast
из A в
B возможен, когда B в A может быть преобразовано неявно.
Компилятор бдительно следит за
использованием static_cast
и прилагает все усилия, чтобы
приведение было осуществлено безопасно. Скорее всего, если вы имеете
дело с заведомо небезопасным приведением, то он вам просто не даст
использовать static_cast
, скорее вам придется прибегнуть к
reinterpret_cast
. Хотя, как было видно в самом первом примере
компилятор не в состоянии полностью гарантировать безопасность
приведения -- но на этот случай есть dynamic_cast
. В подавляющем
большинстве случаев старые C'шные преобразования типов можно спокойно
поменять на static_cast
. Места, которые после этого не
откомпилируются, скорее всего, потенциальный источник ошибок.
Необходимо понимать, что static_cast
(в отличие от
dynamic_cast
) выполняется исключительно во время
компиляции, и потому часть проверок осуществлять не может
и не будет. Важный пример -- приведение указателя на базовый
к указателю на порождённый тип. static_cast
проверит
только, что типы в соответствующем отношении находятся.
dynamic_cast
(если сможет) убедится, что перед нами --
действительно объект порождённого класса, и что лишь указатель --
на базовый.
Этот оператор в каком-то смысле противоположность оператору
static_cast
. Он применяется для приведений между несоотносимыми
типами -- между char* и int*, между указателем и int, между
объектами, не связанными отношением наследования и т.п. Приведения
такого рода не могут гарантировать никакой корректности. По существу
приведение reinterpret_cast<T>(p)
просто заставляет
интерпретировать кусок памяти, в котором размещена переменная p так,
как будто там размещена переменная типа T. Поскольку размещение
переменных в памяти зависит от реализации, на результаты работы
reinterpret_cast
нельзя полагаться ни в одном из случаев за
исключением приведения к исходному типу. Все остальные приведения в
лучшем случае непереносимы. reinterpret_cast
-- средство для
низкоуровневых, почти ``хакерских'' операций, прибегать к которому
следует только хорошенько подумав действительно ли без этого никак
нельзя. Может показаться неожиданным, но нотация
reinterpret_cast
не может быть использована для навигации по
иерархии классов, даже если объекты связаны отношением
наследования. Например:
#include <iostream>
class A { double x; };
class B { char c; };
class D : public A, public B { /* ... */ };
void f(B* pb)
{
D* pd1 = reinterpret_cast<D*>(pb);
D* pd2 = static_cast<D*>(pb);
std::cout << "pd1: " << pd1 << std::endl << "pd2: " << pd2 << std::endl;
}
int main(int argc, char **argv)
{
f(new D);
}
Выдача программы, естественно меняется от вызова к вызову, но
указатели pd1 и pd2 всегда ссылаются на разные адреса. Вот пример
выдачи:
pd1: 002F0918
pd2: 002F0910
В данном примере pd2 будет указывать на начало переданного объекта D и
это правильно. Указатель pd1 будет указывать на начало подобъекта B в
объекте D, и при этом он будет думать, что он указывает на объект типа
D. Скорее всего, попытка что-нибудь сделать с таким объектом приведет
к краху программы.
Может возникнуть естественный вопрос -- если reinterpret_cast
такое
опасное и почти никогда не нужное средство, то зачем тогда вообще было
его вносить в язык. Краткий ответ -- врага надо знать в лицо. Если бы
такой нотации не было, то это еще не значит, что не было бы таких
приведений - просто они бы скрывались за C'шным синтаксисом. Введение
такой нотации позволяет, во-первых, осознано применять такие
приведения, а во-вторых, легко отслеживать места, где эти приведения
используются.
Компилятор всегда очень пристально следит за использованием атрибута
const
. Он старается выдерживать принцип ``единожды константный всегда
остается константным''. В разных ситуациях это проявляется
по-разному. Так, например, если формальный аргумент функции
неконстантный, то и фактический аргумент, используемый при вызове
должен быть неконстантным:
void f(int *)
{
// ...
}
int i=17;
const int* p = &i;
const int j=29;
f(&i); // Можно, потому что i - не константа
f(p); // Нельзя, потому p - указатель на константу
f(&j); // Тоже нельзя, потому что j - константа
Здесь даже не важно, действительно ли внутри f значение по переданному
указателю изменяется, компилятору достаточно того, что это может
произойти. Если вы точно знаете, что значение по переданному указателю
не изменится, то объявляйте функцию как void f(const int *)
и пример
выше заработает. Другой пример слежки за константностью связан с
константными методами. Внутри таких методов переменная this фактически
объявляется как const Foo* this
(где Foo -- имя класса).
class Foo {
private:
int x;
public:
void f() const;
void g();
};
void h(int*);
void m(foo*);
void Foo::f()
{
x = 17; // Нельзя: изменится переменная класса
this->g(); // Нельзя: g - неконстантная функция
// и может изменить переменные класса
h(&x); // Нельзя: h может изменить x
m(this); // Нельзя: неконстантный аргумент в m()
}
Поскольку метод f объявлен как константный, то компилятор следит за
тем, чтобы переменные класса не могли измениться не только явно (как в
случае с x = 17
), но и косвенно (как в остальных случаях).
Однако компилятор не может гарантировать, что константность никогда не
снимется -- ее можно снять с помощью приведений типов. Заметим, что
операторы static_cast
, dynamic_cast
и даже
reinterpret_cast
не могут снять оператор константности:
void f(const Object* pc)
{
Object* pm;
pm = static_cast<Object*>(pc); // Ошибка на уровне компиляции
pm = dynamic_cast<Object*>(pc); // Ошибка на уровне компиляции
pm = reinterpret_cast<Object*>(pc); // Ошибка на уровне компиляции
pm = (Object*)pc; // Нет ошибки
}
Чтобы явно указать, что цель приведения -- избавиться от атрибута const
,
используется оператор const_cast
:
pm = const_cast<Object*>(pc); // Нет ошибки
Хотя результат будет таким же как и в случае с приведением в стиле С,
такая нотация лучше, поскольку легко интерпретируется вне контекста и
легко отслеживается в тексте программы.
Footnotes
- ... этому1
- Секрет золотого
ключика, впрочем, прост: достаточно поинтересоваться оплатой
переводчиков.
- ...
нет2
- Ещё раз напомню, что сами регулярные выражения -- есть, если
бы мне понадобилось, я бы сначала смотрел в сторону boost::regex.
Мне, впрочем пока не надобилось.
- ... место3
- Упражнение: почему STL спроектирована
именно так?
- ... ломается4
- Здоровых нет, есть плохо
продиагностированные!
- ... пионерство5
- Здесь мы, видимо, страдаем из-за отсутствия
в языке неопределённых значений.
- return 0; }6
- Как вы могли заметить, я всюду стараюсь писать
const. Это продиктовано всё теми же соображениями читаемости, а не,
скажем, эффективностью. Простейший способ сагитироваться в ряды
constописателей -- поискать ответа на вопрос ``Какой гад это сюда пишет?''
в не слишком маленькой чужой программе. Известный борец с
const_iterator Мейерс со своим советом 26 идёт советовать в
другое место.
- ... неочевидных7
- Загляните
внутрь -- не пожалеете!
- ... моментально8
- Упражнение: проделайте ЭТО.
- ... COW9
- Copy-on-Write.
- ... оказаться10
- А может и
не оказаться.
- ...
непригодны11
- Упражнение: почему?
- ... Publish-Subscribe12
- Он же
Observer-Observable
2003-09-16