
Сейчас репутация Singleton давно не та, что была ранее. В последние годы он признается антипаттерном, который следует избегать в новом коде, но что делать с legacy кодом? Ловить косые взгляды современников? В данной статье мы напишем плагин для поиска этого паттерна в коде на C++, разберем аспекты разработки плагина с помощью Clang API и протестируем плагин на реальных проектах.
Примечание: данная статья не несет цели вступать в полемику о правильности использования паттерна; данная статья посвящена только разработке плагина.
Назначение
Плагин будет искать классы, а также функции, предположительно созданные для реализации Singleton. Здесь стоит внести конкретику. Плагин будет находить такие реализации Singleton как Naive Singleton, Meyer's Singleton, CRTP Singleton, а также функцию для создания Singleton:
Функция single
template<typename T>
static T& single(){
static T instance;
return instance;
}Подготовка среды
Начнем с инструментария. В качестве архитектуры для анализа кода я выбрал AST Visitor, вместо AST Matcher. По большей мере, потому что данный вариант был привычен для меня и выделялся своей гибкостью и простотой.
Язык программирования C++17. Версия clang/llvm 20.1.8.
Составил следующий Makefile для сборки проекта:
Makefile
LLVM_FLAGS = $(shell llvm-config --cxxflags --ldflags --system-libs --libs core)
SOURCE ?= source.cpp
DEV_FLAGS = -std=c++17 -fno-rtti -fPIC -shared -g -O1 -ferror-limit=3
all: clean SingletonChecker.so test
SingletonChecker.so:
clang++ $(DEV_FLAGS) -I$(shell llvm-config --includedir) SingletonCheckerMain.cpp -o SingletonChecker.so $(LLVM_FLAGS)
test: SingletonChecker.so $(SOURCE)
clang++ -fsyntax-only -Xclang -load -Xclang ./SingletonChecker.so -Xclang -plugin -Xclang class-visitor $(SOURCE)
clean:
rm -f SingletonChecker.so
.PHONY: all test clean
Компиляция плагина занимает не так много времени, чтобы выпить пару чашек горячего чая, но и не так мало, чтобы ее не успеть прервать, но достаточно для того, чтобы заскучать. Дабы это не осложняло дальнейшую разработку, добавим такие ключи как
-fsyntax-only- говорит компилятору сделать лишь синтаксический анализ, не переходя далее к последующим этап компиляции.
И вполне типичные флаги для сборки под debug:-g -O1 -ferror-limit=3.
Реализация плагина
Я решил разбить анализ на 5 этапов, — четыре из которых относятся непосредственно к анализу, а пятый представляет из себя составление отчета об анализе подозрительных классов.
Сама суть анализа заключается в сужении множества мест, где может быть создан объект класса, и последующим осмотром данных мест. То есть, если мы выяснили, что конструкторы класса являются приватными, то множество мест, где может быть создан объект обозреваемого класса сужается до друзей, статических полей, методов, а также включающих и внешних классов класса. Но обо всем по порядку.
Первый этап
На этом этапе рассматриваются общие черты класса: инкапсуляция конструкторов, инкапсуляция операторов присваивания, а также их существование в принципе. В дополнение, на данном этапе будет осуществляться поиск функции с сигнатурой схожей с той, что обычно бывает у методов getInstance() в вышеупомянутых реализациях Singleton.
Для удобства восприятия кода прикрепил диаграмму проверок первого этапа:

И сама реализация:
bool VisitCXXRecordDecl(CXXRecordDecl *declaration) {
// ...
// first stage of analysis
for (const auto* c : declaration->ctors()) {
if (c->getAccess() == AS_public && !c->isDeleted()) {
analysisData.ctorsPrivate = false;
break;
}
}
if (!analysisData.ctorsPrivate)
return true;
analysisData.hasDeletedCopyConstuctors = true;
analysisData.hasDeletedAssigmentOperators = true;
for (auto *method : declaration->methods()) {
if (method->isStatic() && !analysisData.hasMethodLikelyInstance) {
analysisData.hasMethodLikelyInstance = isProbablyGetInstanceFunction(method);
if (analysisData.hasMethodLikelyInstance) {
analysisData.hiddenInstanceMethod = method->getAccess() != AS_public;
analysisData.probabalyCRTPSingleton = method->getReturnType()->isDependentType();
}
analysisData.hasMethodLikelyInstance &= compareReturnTypeWithRecordType(method, declaration)
|| analysisData.probabalyCRTPSingleton;
}
if (CXXConstructorDecl* ctrDecl = dyn_cast<CXXConstructorDecl>(method)) {
if (ctrDecl->isCopyOrMoveConstructor())
analysisData.hasDeletedCopyConstuctors &= ctrDecl->isDeleted();
}
if (method->isCopyAssignmentOperator())
analysisData.hasDeletedAssigmentOperators &= method->isDeleted();
// second stage of analysis
// ...
}Забегая немного вперед, скажу, что isProbablyGetInstanceFunction находит признаки специфичные для Naive и Meyer's Singleton, но реализация getInstance в CRTP зачастую схожа с раннее указанными, поэтому для обнаружения признаков CRTP достаточно проверить является ли возвращаемый тип getInstance шаблонным. В остальном код повторяет диаграмму, цепляет взгляд лишь одна туманная вещь, — строки 18 и 23. Давайте обратимся к функциям compareReturnTypeWithRecordType и isProbablyGetInstanceFunction.
Если со вспомогательной функцией compareReturnTypeWithRecordType все ясно:
compareReturnTypeWithRecordType()
bool isSameType(QualType type1, QualType type2) {
return type1.getTypePtr()->getUnqualifiedDesugaredType() ==
type2.getTypePtr()->getUnqualifiedDesugaredType();
}
bool compareReturnTypeWithRecordType(FunctionDecl *method,
CXXRecordDecl *record) {
QualType returnType = method->getReturnType();
QualType recordType = record->getASTContext().getRecordType(record);
if (returnType->isPointerType() || returnType->isReferenceType())
returnType = returnType->getPointeeType();
return isSameType(returnType, recordType);
}Функция как бы вычесывает тип возвращаемого значения функции, а так же тип класса до того, чтобы остались только исходные типы без синонимов, квалификаторов и сахара, затем их сравнивает.
В свою очередь, функция isProbablyGetInstanceFunction (и составляющие ее части) более идейная:
bool isValidSingletonMethodSignature(FunctionDecl *method) {
return method && method->hasBody() &&
(method->getReturnType()->isPointerType() ||
method->getReturnType()->isReferenceType());
}
bool isProbablyGetInstanceFunction(FunctionDecl *method)
{
if (!isValidSingletonMethodSignature(method))
return false;
for (Stmt* stmt : method->getBody()->children()) {
// ...
if (auto *retStmt = dyn_cast<ReturnStmt>(stmt)) {
analyzeReturnStatement(retStmt);
}
else if (auto* ifStmt = dyn_cast<IfStmt>(stmt)) {
analyzeIfStatement(ifStmt);
}
}
return analysisData.probabalyNaiveSingleton ||
analysisData.probablyMeyersSingleton;
}isProbablyGetInstanceFunction принимает заведомо статическую функцию, поэтому в isValidSingletonMethodSignature нам достаточно проверить часть ее сигнатуры. Обратим также внимание на сигнатуру самой функции isProbablyGetInstanceFunction, — к нестандартному, может, стоило использовать CXXMethodDecl, типу принимаемой переменой мы вернемся чуть позже.
Заглянем в составляющую часть анализа выражений isProbablyGetInstanceFunction:
void analyzeReturnStatement(ReturnStmt* retStmt)
{
Expr* retExpr = retStmt->getRetValue();
VarDecl* returnedVar = analyzeReturnExpression(retExpr);
// ...
analysisData.instanceField = returnedVar;
if (returnedVar->isStaticLocal()) {
analysisData.probablyMeyersSingleton = true;
}
else if (returnedVar->isStaticDataMember() &&
returnedVar->getAccess() != AS_public) {
analysisData.probabalyNaiveSingleton = true;
}
}
VarDecl* analyzeReturnExpression(Expr* retExpr)
{
// ...
retExpr = retExpr->IgnoreParenImpCasts();
// Context: return &instance / *instance / instance
VarDecl* returnedVar = extractVarFromUnary(retExpr);
// Context: return () ? T : F;
if (auto* condOp = dyn_cast<ConditionalOperator>(retExpr)) {
returnedVar = analyzeConditionalOperator(condOp);
}
return returnedVar;
}
VarDecl* analyzeConditionalOperator(ConditionalOperator* condOp)
{
auto conditionResult = analysisCondition(condOp->getCond());
VarDecl* conditionVar = conditionResult.extracted;
if (!conditionVar) {
analysisData.unknownPatternSingleton = true;
return nullptr;
}
VarDecl* returnedVar = extractVarFromUnary(condOp->getTrueExpr()) ? :
extractVarFromUnary(condOp->getFalseExpr());
if (returnedVar != conditionVar) {
if (conditionVar->getType()->isBooleanType()) {
analysisData.probablyFlagsNaiveSingleton = true;
} else {
analysisData.unknownPatternSingleton = true;
}
}
return returnedVar;
}
VarDecl* extractVarFromUnary(Expr* expr) {
if (auto* unop = dyn_cast<UnaryOperator>(expr->IgnoreImpCasts())) {
if (unop->getOpcode() == UO_AddrOf || unop->getOpcode() == UO_Deref) {
return getVarDeclFromExpr(unop->getSubExpr()->IgnoreParenCasts());
}
}
return getVarDeclFromExpr(expr->IgnoreImpCasts());
};
VarDecl* getVarDeclFromExpr(Expr* expr) {
if (auto *declRef = dyn_cast<DeclRefExpr>(expr->IgnoreParenCasts()))
return dyn_cast<VarDecl>(declRef->getDecl());
return nullptr;
}
// ... analysisCondition ...Замечание: при написании clang-плагина с помощью архитектуры AST Visitor начало функции обработки чего-либо нередко начинается с проверки валидности указателей, такие строки кода вполне естественны, поэтому я решил вырезать их из листингов.
Еще одна особенность Clang API это узел, являющийся ссылкой на объявление, — будь то переменная, функция или прочее; он же DeclRefExpr. К примеру, в GCC можно напрямую получить VarDecl. Из этого следует, что для получения самого объявления нам нужно обратиться к ссылке на него, что собственно и происходит в getVarDeclFromExpr.
analysisCondition возвращает пару, которая представляет из себя паттерн условия и переменную из условия. К примеру, для такого паттерна условия: instance == nullptr будет возвращено: extracted = instance, param = BinaryOperatorInConditionNullptr. Данная структура поможет нам в будущем составить более полный отчет об анализе класса. Реализацию функции analysisConditionвы можете найти на GitHub.
Готов вас обрадовать ведь всё, что мы уже написали способно найти такие методы getInstance как
Такие методы как
// | -------------------------1-----------------------------|
// | Будет выставлен признак probablyCRTPSingleton |
// | и probablyMeyersSingleton |
// | -------------------------------------------------------|
static T& getInstance()
{
static T instance;
return instance;
}
// | --------------------------2----------------------------|
// | Будет выставлен признак probablyMeyersSingleton |
// | -------------------------------------------------------|
static X& getInstance() {
static X* instance = new X();
return *instance;
}
// | --------------------------3----------------------------|
// | Будет выставлен признак probablyNaiveSingleton |
// | Примечание: работает и наоборот instance != nullptr |
// | -------------------------------------------------------|
static const NaiveSingleton* getInstance() {
return (nullptr != instance) ? instance
: instance = new NaiveSingleton();
}
// | -------------------------4-----------------------------|
// | Будет выставлен признак probablyNaiveSingleton |
// | и probablyFlagsNaiveSingleton |
// | -------------------------------------------------------|
NaiveSingleton() {
isCreated = true;
}
static const NaiveSingleton* getInstance() {
return (isCreated) ? instance
: instance = new NaiveSingleton();
}
// | --------------------------5----------------------------|
// | Будет выставлен признак probablyNaiveSingleton |
// | Примечание: работает и с &instance |
// | -------------------------------------------------------|
static const NaiveSingleton& getInstance() {
return (nullptr != instance)? *instance
: *(instance = new NaiveSingleton());
}
Но это еще не все, кроме тернарного оператора, может быть и старый добрый if. Для анализа такого случая было написано следующее:
Анализ выражения If в getInstance()
void analyzeIfStatement(IfStmt* ifStmt)
{
Expr* condition = ifStmt->getCond();
auto conditionResult = analysisCondition(condition);
VarDecl* conditionVar = conditionResult.extracted;
Stmt* thenBody = ifStmt->getThen();
analysisData.probablyFlagsNaiveSingleton = conditionVar->getType()->isBooleanType();
std::vector<BinaryOperator*> assignments;
findAssignmentsInStmt(thenBody, assignments);
for (auto* assign : assignments) {
if (assign->getOpcode() == BO_Assign) {
VarDecl* assignedVar = getVarDeclFromExpr(assign->getLHS());
if (assignedVar && assignedVar == conditionVar) {
if (assignedVar->isStaticDataMember() &&
assignedVar->getAccess() == AS_private) {
analysisData.probablyIfNaiveSingleton = true;
analysisData.assignmentInIfSingleton = assign;
break;
}
}
}
}
}
Для тернарного оператора я посчитал, что проверка на присвоение в одной из веток не сильно принципиальна, потому как достаточно проверить ситуацию возврата переменной в любой ветке.
В случае с if все наоборот ведь, наверное, для каждого паттерн Singleton начинался с такой реализации:
if в getInstance()
// | --------------------------6----------------------------|
// | Будет выставлены признаки: probabalyNaiveSingleton , |
// | probabalyIfNaiveSingleton |
// | Примечание: работает nullptr / NULL |
// | -------------------------------------------------------|
// пример из книги Design Patterns
Singleton* Singleton::Instance () {
if (_instance == 0) {
_instance = new Singleton;
}
return _instance;
} Это одно из наиболее распространенных воплощений Singleton, поэтому analyzeIfStatement заточен конкретно под этот вариант.
Второй этап
Наконец-то можем перейти к следующему этапу, он намного проще в понимании, чем первый: меньше кода, слов, идей. Начнем с диаграммы.

На прошлом этапе мы выяснили, что создать объект анализируемого класса возможно только в пространстве самого класса, его друзей или внешних, внутренних классов, и убедились в том, что скопировать объект в привычном понимании высокоуровневого кода не получится, так как удалены копирующий конструктор и оператор. Узнав это, пора проверить, а действительно ли объект single.
for (auto *method : declaration->methods()) {
// ...
// second stage of analysis
analysisData.amountObjects += countClassStaticObject(declaration, method);
if (analysisData.amountObjects > 1 || findClassLocalObject(declaration, method)) {
analysisData.isSingleton = false;
break;
}
}
// second stage of analysis
if (analysisData.isSingleton) {
for (auto* field : declaration->decls()) {
if (isClassObject(dyn_cast<VarDecl>(field), declaration)) {
if (++analysisData.amountObjects > 1) {
analysisData.isSingleton = false;
break;
}
}
}
}Данный фрагмент кода вполне самодокументирован, перейдем сразу к менее абстрактным функциям:
countClassStaticObject()
int countClassStaticObject(CXXRecordDecl* clssDecl, FunctionDecl* funcDecl)
{
int count = 0;
for (Stmt* st : funcDecl->getBody()->children()) {
if (auto *declStmt = dyn_cast<DeclStmt>(st)) {
count += count_if(declStmt->decl_begin(), declStmt->decl_end(),
[&](VarDecl* var) {
return isClassObject(var, clssDecl)
&& var->isStaticLocal();
}
);
}
}
return count;
}
template<typename InputIt, typename UnaryPred>
typename std::iterator_traits<InputIt>::difference_type
count_if(InputIt first, InputIt last, UnaryPred p)
{
using Cast = std::remove_pointer_t<typename function_traits<UnaryPred>::arg_type>;
typename std::iterator_traits<InputIt>::difference_type ret = 0;
for (; first != last; ++first) {
if (auto *c = dyn_cast<Cast>(*first))
if (p(c))
++ret;
}
return ret;
}В Clang API предусмотрены итераторы по дочерним узлам соответствующего AST. Согласен, что это можно было увидеть и ранее ведь мы ��спользовали range-based for. Но в данном фрагменте кода продемонстрирована отличная связка почти стандартного алгоритма и Clang API.
Да, как можно заметить, реализация алгоритма нестандартная. Мне пришлось добавить каст к типу узла, который нас интересует при подсчете, так я потерял гибкость функции, но для нашего проекта такой вариант будет очень полезен.
Функция findClassLocalObject реализована схожим образом, только выполняет поиск до первого нестатического объекта.
isClassObject()
template<typename T>
bool isClassObject(T* varDecl, CXXRecordDecl* clssDecl)
{
return !varDecl->getType()->isPointerType() && !varDecl->getType()->isReferenceType()
&& (varDecl->getType()->getCanonicalTypeUnqualified() == clssDecl->getTypeForDecl()->getCanonicalTypeUnqualified());
}Ну, и вспомогательная функция, которая проверяет является ли VarDecl объектом определенного класса.
Теперь мы избавились от ложных срабатываний нашего плагина в подобных ситуациях:
Ситуации, когда singleton не single
class X
{
private:
X() {}
public:
X(const X&) = delete;
X& operator=(const X&) = delete;
static X& getInstance() {
static X instance;
static X instance1;
return instance;
}
};Третий этап
Мы закончили с обзором внутреннего устройства анализируемого класса.
Этот этап анализа посвящен разбору мест вне класса, где мы можем наткнуться на определения instance или на нестатический объект класса. В принципе суть та же, что и на втором и первом этапе, но теперь мы шмонаем анализируем друзей класса.

И соответствующий код:
// ...
for (FriendDecl* friendDecl : declaration->friends()) {
if (!analysisData.isSingleton) break;
if (NamedDecl* nd = friendDecl->getFriendDecl()) {
if (FunctionDecl* funcFriend = dyn_cast<FunctionDecl>(nd)) {
if (compareReturnTypeWithRecordType(funcFriend, declaration))
updateFriendGetInstanceCandidate(funcFriend);
checkObjectViolations(declaration, funcFriend);
}
}
else if (TypeSourceInfo* tsi = friendDecl->getFriendType()) {
QualType qt = tsi->getType();
if (const RecordType* rt = qt->getAs<RecordType>()) {
if (CXXRecordDecl* friendClss = dyn_cast<CXXRecordDecl>(rt->getDecl())) {
checkObjectViolations(declaration, friendClss);
for (CXXMethodDecl* friendMethod : friendClss->methods())
updateFriendGetInstanceCandidate(friendMethod);
}
}
}
}
void updateFriendGetInstanceCandidate(FunctionDecl* funcFriend)
{
if (!analysisData.hasFriendFunctionLikelyInstance) {
analysisData.hasFriendFunctionLikelyInstance =
isProbablyGetInstanceFunction(funcFriend);
if (analysisData.hasFriendFunctionLikelyInstance)
analysisData.friendFunctionLikeGetInstance = funcFriend;
}
}
template<typename T>
void checkObjectViolations(CXXRecordDecl* declaration, T* decl)
{
analysisData.amountObjects += countClassStaticObject(declaration, decl);
if (analysisData.amountObjects > 1 ||
findClassLocalObject(declaration, decl))
analysisData.isSingleton = false;
}
В этом фрагменте мы оперируем уже рассмотренными функциями. Кстати, здесь и кроется объяснение сигнатуры isProbablyGetInstanceFunction, на которой я заострял внимание ранее.
Есть одна интересная деталь, связанная с Clang API, а именно строки 12-14. Это появление TypeSourceInfo. В данной ситуации мы не можем использовать единообразный подход для получения объявления дружественной сущности, к примеру, дружественных классов и функций. Чтобы лучше разобрат��ся в проблеме приведу такой листинг программы:
class X
{
private:
X() {}
public:
X(const X&) = delete;
X& operator=(const X&) = delete;
friend X& getInstance(); // Объявляет новую функцию
friend class Y; // Ссылается на тип
};
class Y {
static X hidden_instance;
};
X& getInstance() {
static X instance;
return instance;
}Здесь объявление getInstance будет получено посредством getFriendDecl. В свою очередь, friend class Y не является прямым объявлением и не может быть получено с помощью getFriendDecl. В данном случае это выражение означает лишь отношение дружбы классов и ссылается на тип. Поэтому путь до объявления дружественного класса лежит через TypeSourceInfo и QualType.
Конечно, этот пример кода также является детектируемым нашим плагином и после анализа amountObjects = 2, и соответственно признак isSingleton = false.
Четвертый этап
Представляет собой обход функций в целях поиска той самой, что порождает Singleton, — о ней я упоминал в начале.
Сразу перейдем к реализации, ведь она у нас уже есть:
bool VisitFunctionDecl(FunctionDecl *func)
{
// ...
if (!func->getReturnType()->isPointerType()
&& !func->getReturnType()->isReferenceType())
return true;
if(isProbablyGetInstanceFunction(func))
printInfoFunc(func);
return true;
}
Как и видно, мы просто используем уже написанную функцию isProbablyGetInstanceFunction.
Замечание по листингам: код приводится в упрощенном виде, если вы заглянете на страницу GitHub с проектом, то увидите немного другую архитектуру кода, такая рознь обусловлена соблюдением баланса между читаемостью статьи и раскрытием идей анализа.
Пятый этап
Здесь мы выведем отчет о проведенном нами анализе подозрительных классов, другими словами, объясним, почему мы их подозреваем.
Интересен этот раздел статьи тем, что я продемонстрирую вывод отчета на примере разбора реального проекта, а именно snapcast. Выбрал я его не просто так, а потому что в нем встречаются как классы-Singleton, так и функции-Singleton.
Собирать весь проект мы не будем, нас интересует только синтаксический анализ, поэтому игнорируем cmake, предоставленный разработчиком, заместо него составим, неожиданно, bash-скрипт.
Запускаем синтаксический анализ для каждого исходника
CLANG=$(which clang++ || which clang)
if [ -z "$CLANG" ]; then
echo "Error: clang++ not found"
exit 1
fi
if [ ! -f "SingletonChecker.so" ]; then
echo "Error: SingletonChecker.so not found in current directory"
echo "Current directory: $(pwd)"
ls -la *.so 2>/dev/null || echo "No .so files found"
exit 1
fi
echo "Using compiler: $CLANG"
echo "Plugin location: $(pwd)/SingletonChecker.so"
SOURCES=$(find . -name "*.cpp" -o -name "*.hpp" )
echo "Found $(echo "$SOURCES" | wc -l) source files"
for file in $SOURCES; do
echo "Analyzing: $file"
$CLANG -fsyntax-only \
-Xclang -load -Xclang "./SingletonChecker.so" \
-Xclang -plugin -Xclang class-visitor \
"$file" \
-std=c++17 -I. -I./common -I./server -I./client
doneДавайте запустим скрипт и посмотрим, что нам удастся найти.

Давайте не будем верить плагину на слово и проверим действительно ли это Singleton. Заглянем в исходник:
// client/time_provider.hpp
//...
class TimeProvider
{
public:
/// @return singleton
static TimeProvider& getInstance()
{
static TimeProvider instance;
return instance;
}
//...Действительно, плагин нам указал никак иначе как на Meyer's Singleton, определил правильную переменную instance и вывел подробную информацию о признаках, которые он смог или не смог выявить. Давайте рассмотрим остальной вывод плагина и подведем итог.

// common/aixlog.hpp
//...
class Log : public std::basic_streambuf<char, std::char_traits<char>>
{
public:
static Log& instance()
{
static Log instance_;
return instance_;
}
//...Как я и говорил в проекте есть не только классы Singleton, но и функция. Плагин сообщил нам следующее:

И исходник:
// server/authinfo.cpp
// ...
const std::error_category& category()
{
// The category singleton
static detail::category instance;
return instance;
}
// ...Итог
В ходе статьи нам удалось разработать простой инструмент для анализа кода, в частности, поиска Meyer's Singleton, CRTP Singleton, Naive Singleton, а так же функции Singleton. Хочется отметить, что практическое применение плагина показало достойный результат, — были выявлены все одиночки в проекте snapcast (правда, в статье я привел не все, из соображения читаемости статьи).
Также стоит упомянуть о том, что можно было бы улучшить, точнее, до чего не дошли руки:
1) Плагин не обозревает внутренние и внешние классы, — ведь ситуация, когда Singleton появляется в образе внешнего класса еще страннее, чем когда он является внутренним классом, о таком нужно сообщать пользователю.
2) Добавить обработку сложных случаев, — я думаю, что почти каждый видел, как простые указатели с течением времени становятся умными. Поэтому плагин должен работать как с обычными, так и с умными указателями.
Код плагина здесь.