воскресенье, 25 декабря 2011 г.

И снова про синтаксические особенности C++

В C++ очень сложный и неоднозначный синтаксис (гораздо сложнее, чем в том же C). На эту тему было написано немало статей, книг. Ниже я приведу несколько особенностей, на которые я натолкнулся недавно, причем, на некоторые я наступаю не в первый раз.


Безымянные (unnamed) объекты
Безымянный объект - объект, который не имеет своего имени (спасибо, кэп :)).
Самый главная проблема с безымянными объектами - это время их жизни.
Самый простой пример:
std::string getName() { return "sample name"; }
_Winnie C++ Colorizer
Здесь создается временный std::string объект, который возвращается из функции.
Если мы вызовем эту функцию без присваивания, то временный объект создастся перед выходом из функции, и уничтожится, сразу после выхода.

Разберем время жизни безымянных объектов на следующем примере:
#include <iostream> #include <boost/noncopyable.hpp> std::string indentToSpaces(int indent) { return std::string(indent * 2, ' '); } #define INDENT() indentToSpaces(visibility_guard::indentation) struct visibility_guard: private boost::noncopyable { visibility_guard(const std::string & name) { std::cout << INDENT() << name << std::endl; std::cout << INDENT() << "{" << std::endl; ++indentation; } ~visibility_guard() { --indentation; std::cout << INDENT() << "}" << std::endl; } static int indentation; }; int visibility_guard::indentation = 0; #define VISIBILITY_GUARD(name) visibility_guard visibilityGuard(name) struct unnamed_obj { static int objCounter; unnamed_obj() { ++objCounter; std::cout << INDENT() << "ctor(), count: " << objCounter << ", " "this: " << static_cast<const void *>(this) << std::endl; } unnamed_obj(const std::string & param) { ++objCounter; std::cout << INDENT() << "ctor(param), count: " << objCounter << ", " "this: " << static_cast<const void *>(this) << std::endl; } ~unnamed_obj() { --objCounter; std::cout << INDENT() << "dtor(), count: " << objCounter << ", " "this: " << static_cast<const void *>(this) << std::endl; } unnamed_obj(const unnamed_obj & other) { ++objCounter; std::cout << INDENT() << "ctor(copy), count: " << objCounter << ", " "this: " << static_cast<const void *>(this) << std::endl; } }; int unnamed_obj::objCounter = 0;
_Winnie C++ Colorizer

Специальный класс visibility_guard будем использовать для диагностики области видимости объектов. Также есть механизмы для улучшения читабельности результат нашей программы - это макросы INDENT и VISIBILITY_GUARD
Итак, пример номер раз:
unnamed_obj getObj() { VISIBILITY_GUARD("getObj"); return std::string("sample obj"); } void unusedObj() { VISIBILITY_GUARD("unusedObj"); getObj(); } int main() { unusedObj(); }
_Winnie C++ Colorizer
Результат:
unusedObj { getObj { ctor(param), count: 1, this: 0xbfb093af } dtor(), count: 0, this: 0xbfb093af }
_Winnie C++ Colorizer

Итак мы видим, что временный объект создался внутри функции getObj, и разрушился сразу после выхода из функции.

Для временных объектов есть возможность продлить их время жизни. Если мы присвоим временный объект константной ссылке (вернее, инициализируем ссылку с помощью временного объекта), то временный объект будет жить вместе с этой константной ссылкой.
Пример номер два:
void prolongLifeTime() { VISIBILITY_GUARD("prolongLifeTime"); const unnamed_obj & reference = getObj(); getObj(); { VISIBILITY_GUARD("prolongLifeTime2"); } } //////////////////////////////////////////////////////////// int main() { prolongLifeTime(); }
_Winnie C++ Colorizer
Результат:
prolongLifeTime { getObj { ctor(param), count: 1, this: 0xbfd638f2 } getObj { ctor(param), count: 2, this: 0xbfd638f3 } dtor(), count: 1, this: 0xbfd638f3 prolongLifeTime2 { } dtor(), count: 0, this: 0xbfd638f2 }
_Winnie C++ Colorizer

Здесь мы видим, что временный объект 0xbfd638f2 живет до конца области видимости переменный reference, то есть его время жизни продлилось (я специально вставил макрос prolongLifeTime2 чтобы показать, что временный объект уничтожился в самом конце.

Есть неприятная особенность: можно создать объект, вызвав конструктор (но не создавая никакой переменной):
void showLifeTimeForUnnamedObject() { VISIBILITY_GUARD("showLifeTimeForUnnamedObject"); unnamed_obj(); { VISIBILITY_GUARD("showLifeTimeForUnnamedObject After"); } } int main() { showLifeTimeForUnnamedObject(); }
_Winnie C++ Colorizer
В данном примере мы создаем временный объект, который разрушается сразу после создания, как видно на результате:
showLifeTimeForUnnamedObject { ctor(), count: 1, this: 0xbff2e117 dtor(), count: 0, this: 0xbff2e117 showLifeTimeForUnnamedObject After { } }
_Winnie C++ Colorizer
Я также добавил вызов VISIBILITY_GUARD, чтобы показать время жизни объекта. Как я и сказал, объект разрушается сразу после создания.

Можно создать временный объект с помощью конструктора с аргументами, результат получается аналогичный:
void showLifeTimeForUnnamedObjectWithParam() { VISIBILITY_GUARD("showLifeTimeForUnnamedObjectWithParam"); unnamed_obj("some parameter"); { VISIBILITY_GUARD("showLifeTimeForUnnamedObjectWithParam After"); } } int main() { showLifeTimeForUnnamedObjectWithParam(); }
_Winnie C++ Colorizer
Результат:
showLifeTimeForUnnamedObjectWithParam { ctor(param), count: 1, this: 0xbfadd45f dtor(), count: 0, this: 0xbfadd45f showLifeTimeForUnnamedObjectWithParam After { } }
_Winnie C++ Colorizer

Я видел баг, связанный с созданием безымянного объекта: создавался объект, который блокировал мьютекс, но этот объект был безымянным, и поэтому мьютекс сразу же разблокировался. Естественно, происходили всякие странные вещи, которые было невозможно обяснить: портилась переменная, которая должна была защищаться этим мьютексом.

Неинтуитивный синтаксис
Но  время жизни объекта - это еще не самое страшное, можно сделать так, что объект вообще создан не будет, вместо этого будет декларация функции:
void showLifeTimeForUnnamedObjectWithParam2() { VISIBILITY_GUARD("showLifeTimeForUnnamedObjectWithParam2"); //unnamed_obj o = unnamed_obj(unnamed_obj("some parameter")); { VISIBILITY_GUARD("showLifeTimeForUnnamedObjectWithParam2 After"); } } int main() { showLifeTimeForUnnamedObjectWithParam2(); }
_Winnie C++ Colorizer
Я ожидал, что будет создано два временных объекта: один с помощью конструктора с параметром, второй - с помощью копирующего конструктора, но вместо этого я увидел вызов только одного конструктора, второй объект не создавался:
showLifeTimeForUnnamedObjectWithParam2 { ctor(param), count: 1, this: 0xbfbff55f dtor(), count: 0, this: 0xbfbff55f showLifeTimeForUnnamedObjectWithParam2 After { } }
_Winnie C++ Colorizer
Более того, если раскомментировать строку в последнем примере (unnamed_obj o =), то результат изменится незначительно (деструктор вызовется при выходе из функции). То есть происходит не то, что я ожидал увидеть, внешний вызов unnamed_obj(...) на самом деле вообще ничего не делает.

Ну и напоследок, еще один пример:
void showLifeTimeForUnnamedObjectWithParam3() { VISIBILITY_GUARD("showLifeTimeForUnnamedObjectWithParam3"); unnamed_obj(unnamed_obj()); //unnamed_obj(); { VISIBILITY_GUARD("showLifeTimeForUnnamedObjectWithParam3 After"); } } int main() { showLifeTimeForUnnamedObjectWithParam3(); }
_Winnie C++ Colorizer
Я ожидал, что создастся безымянный объект с помощью конструктора без параметров. (Как мы уже выяснили ранее, второй объект не создастся), но тут меня ждал еще один сюрприз:
showLifeTimeForUnnamedObjectWithParam3 { showLifeTimeForUnnamedObjectWithParam3 After { } }
_Winnie C++ Colorizer

Тут вообще ничего не создалось! Вместо этого, на самом деле мы объявили функцию с именем unnamed_obj, и сигнатурой typedef unnamed_obj (* func_signature)(); Это легко проверить, раскомментировав строку в последнем примере и попытавшись скомпилировать, мы получим примерно следующее сообщение:

g++ main.cpp 
/tmp/ccKeKOi3.o: In function `showLifeTimeForUnnamedObjectWithParam3()':
main.cpp:(.text+0xa61): undefined reference to `unnamed_obj()'
collect2: выполнение ld завершилось с кодом возврата 1

То есть, на этапе линковки мы не представили нужной функции (если ее определить, то все прекрасно компилируется).

Итог
Безымянные объекты в C++ (и их способ определения), таят в себе множество опасностей. Эти опасности связаны с тем, что синтаксис языка не всегда интуитивен, и программист в результате получает совсем другое поведение (а не то, на которое он рассчитывал). Причем я не смог добиться от компилятора предупреждения: для gcc использовал флаги -Wall -Wextra -pedantic -Weffc++.


Как с этим бороться?
Очевидно, что не использовать безымянные объекты не получится (так как они возникают слишком часто, и программист не всегда может увидеть их возникновение). Но можно минимизировать их появление, для этого надо не лениться создавать именованные объекты, создавать переменные. Также необходимо придерживаться стиля кодирования, проводить регулярные code-review на предмет спорных участков. Возможно, использование утилит для статического анализа кода может помочь.

Комментариев нет:

Отправить комментарий