В C++ очень сложный и неоднозначный синтаксис (гораздо сложнее, чем в том же C). На эту тему было написано немало статей, книг. Ниже я приведу несколько особенностей, на которые я натолкнулся недавно, причем, на некоторые я наступаю не в первый раз.
Безымянные (unnamed) объекты
Безымянный объект - объект, который не имеет своего имени (спасибо, кэп :)).
Самый главная проблема с безымянными объектами - это время их жизни.
Самый простой пример:
Здесь создается временный std::string объект, который возвращается из функции.
Если мы вызовем эту функцию без присваивания, то временный объект создастся перед выходом из функции, и уничтожится, сразу после выхода.
Разберем время жизни безымянных объектов на следующем примере:
Специальный класс visibility_guard будем использовать для диагностики области видимости объектов. Также есть механизмы для улучшения читабельности результат нашей программы - это макросы INDENT и VISIBILITY_GUARD
Итак, пример номер раз:
Результат:
Итак мы видим, что временный объект создался внутри функции getObj, и разрушился сразу после выхода из функции.
Для временных объектов есть возможность продлить их время жизни. Если мы присвоим временный объект константной ссылке (вернее, инициализируем ссылку с помощью временного объекта), то временный объект будет жить вместе с этой константной ссылкой.
Пример номер два:
Результат:
Здесь мы видим, что временный объект 0xbfd638f2 живет до конца области видимости переменный reference, то есть его время жизни продлилось (я специально вставил макрос prolongLifeTime2 чтобы показать, что временный объект уничтожился в самом конце.
Есть неприятная особенность: можно создать объект, вызвав конструктор (но не создавая никакой переменной):
В данном примере мы создаем временный объект, который разрушается сразу после создания, как видно на результате:
Я также добавил вызов VISIBILITY_GUARD, чтобы показать время жизни объекта. Как я и сказал, объект разрушается сразу после создания.
Можно создать временный объект с помощью конструктора с аргументами, результат получается аналогичный:
Результат:
Я видел баг, связанный с созданием безымянного объекта: создавался объект, который блокировал мьютекс, но этот объект был безымянным, и поэтому мьютекс сразу же разблокировался. Естественно, происходили всякие странные вещи, которые было невозможно обяснить: портилась переменная, которая должна была защищаться этим мьютексом.
Неинтуитивный синтаксис
Но время жизни объекта - это еще не самое страшное, можно сделать так, что объект вообще создан не будет, вместо этого будет декларация функции:
Я ожидал, что будет создано два временных объекта: один с помощью конструктора с параметром, второй - с помощью копирующего конструктора, но вместо этого я увидел вызов только одного конструктора, второй объект не создавался:
Более того, если раскомментировать строку в последнем примере (unnamed_obj o =), то результат изменится незначительно (деструктор вызовется при выходе из функции). То есть происходит не то, что я ожидал увидеть, внешний вызов unnamed_obj(...) на самом деле вообще ничего не делает.
Ну и напоследок, еще один пример:
Я ожидал, что создастся безымянный объект с помощью конструктора без параметров. (Как мы уже выяснили ранее, второй объект не создастся), но тут меня ждал еще один сюрприз:
Тут вообще ничего не создалось! Вместо этого, на самом деле мы объявили функцию с именем 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
Как с этим бороться?
Очевидно, что не использовать безымянные объекты не получится (так как они возникают слишком часто, и программист не всегда может увидеть их возникновение). Но можно минимизировать их появление, для этого надо не лениться создавать именованные объекты, создавать переменные. Также необходимо придерживаться стиля кодирования, проводить регулярные code-review на предмет спорных участков. Возможно, использование утилит для статического анализа кода может помочь.
Безымянные (unnamed) объекты
Безымянный объект - объект, который не имеет своего имени (спасибо, кэп :)).
Самый главная проблема с безымянными объектами - это время их жизни.
Самый простой пример:
std::string getName()
{
return "sample name";
}
|
_Winnie C++ Colorizer |
Если мы вызовем эту функцию без присваивания, то временный объект создастся перед выходом из функции, и уничтожится, сразу после выхода.
Разберем время жизни безымянных объектов на следующем примере:
#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 |
Можно создать временный объект с помощью конструктора с аргументами, результат получается аналогичный:
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 |
Ну и напоследок, еще один пример:
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 на предмет спорных участков. Возможно, использование утилит для статического анализа кода может помочь.
Комментариев нет:
Отправить комментарий