...

суббота, 4 сентября 2021 г.

Яндекс принудительно включил рекламу в своем навигаторе

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

Яндекс. Навигатор
Яндекс. Навигатор

До последнего обновления Яндекс.Навигатора в его настройках можно было отключить шесть форматов, в том числе аудио и рекламу на остановке. Однако теперь эта опция недоступна.

Adindex
Adindex

Пользователи начали оставлять негативные отзывы появились в AppStore и Google Play.

По их словам, рекламные объявления показываются даже при полной остановке автомобиля, а их размер перекрывает значительную часть экрана: «При остановке на маршруте, когда обычно и пытаешься покрутить карту, оценить маршрут, рассмотреть точки поблизости, выскакивает реклама»; «Из-за рекламы, которая вылезает на пол-экрана и отвлекает водителя, приложение стало небезопасным. На ходу постоянно выпрыгивает еще и реклама товаров с “Яндекс Маркет”».

Поддержка Яндекс.Навигатора заявила, что сервис постепенно обновляет настройки отключения рекламы в приложении, поэтому в новой версии такой возможности нет. «Большинство пользователей не отключали рекламу, поэтому мы приняли решение убрать эту опцию из настроек. Реклама — основной источник монетизации приложения и позволяет постоянно развивать технологии навигации», — говорят представители компании.

В Яндексе отметили, что навигатор в автомобильных системах Carplay и Android Auto доступен только в рамках подписки Яндекс Плюс.

В августе Яндекс.Карты и Навигатор добавили поддержку Apple CarPlay и Android Auto, чтобы водители могли строить маршруты и искать места на экранах в автомобилях. Опция стала доступной с подпиской Яндекс Плюс.

Adblock test (Why?)

Создаем новое ключевое слово в C++

Этот код будет компилироваться!
Этот код будет компилироваться!

C++ - один из языков, который можно назвать "легендарным". Его история насчитывает несколько десятилетий, принципы программирования на нем революционным образом менялись не раз, а черновик стандарта уже разросся до 1800+ страниц мелкого шрифта.

На C++ есть много хороших библиотек. Но нередко изменения в самом языке делали неактуальными большие куски кода, потому что они становились менее надёжными и быстрыми по сравнению с функционалом в самом языке. Правки в стандарт имеют несоизмеримо более сильное влияние, чем любая библиотека.

В этой статье мы в учебных целях напишем для C++ поддержку нового ключевого слова defer, которое будет работать во многом аналогично такому в языках Go и Swift. Это будет сделано через правку исходного кода Clang.

Маскот LLVM держит в лапах C++
Маскот LLVM держит в лапах C++

Описание команды defer

В некоторых языках, например в Go и Swift, есть ключевое слово defer. В Go не существует ни исключений (exceptions), ни деструкторов, поэтому идиома для очистки ресурсов - прямое указание языку вызвать cleanup-метод по выходу из функции.

func write(fileName string, text string) error {
    file, err := os.Create(fileName)
    if err != nil {
        return err
    }
    defer file.Close()
    // ... сколько угодно return-ов, file.Close() всегда вызовется
}

Таким образом, C++ не нужен defer, так как аналогичную задачу выполняет идиома RAII. То есть в аналогичном C++-коде был бы использован некий объект, который делает условный os.Create в конструкторе, а file.Close() в деструкторе. Добавление defer в этой публикации несёт иллюстративный характер.


defer в open-source проектах

defer с разным успехом имитировали в существующих проектах "руками".

Boost.ScopeExit - специальная библиотека для имитации defer-а. Она написана на C++11 вперемешку с C++03, поэтому, на мой взгляд, излишне многословна и мало вписывается в текущие реалии.

Хотя Boost - один из самых популярных наборов библиотек, далеко не все его куски находятся в ажурном состоянии. В Boost 167 библиотек, во многом независимых друг от друга. Многие из библиотек либо не обновлялись с ~2006 года, либо стали неактуальными после вхождения их функционала в стандарт, либо повторяют друг друга по функционалу, либо уже есть библиотека вне Boost с лучшим функционалом.

CatBoost - в этой библиотеке для машинного обучения есть хороший вариант Y_DEFER.

Оба варианта самописного defer-а основаны на том, что они скрытым образом создают объект, который в деструкторе будет вызывать нужный код. Для примера рассмотрим хитроумное определение из последней библиотеки:

#define Y_SCOPE_EXIT(...) const auto Y_GENERATE_UNIQUE_ID(scopeGuard) Y_DECLARE_UNUSED = ::NPrivate::TMakeGuardHelper{} | [__VA_ARGS__]() mutable -> void
#define Y_DEFER Y_SCOPE_EXIT(&)

Y_GENERATE_UNIQUE_ID(scopeGuard) генерирует уникальное имя для этого объекта.

Y_DECLARE_UNUSED говорит компилятору не обращать внимание на неиспользуемый объект.

[__VA_ARGS__]() mutable -> void - заготовка для лямбда-выражения. В квадратных скобках захватываемые выражения, по умолчанию туда запишется &. mutable значит, что мы сможем изменять захваченные объекты. -> void это trailing return type, чтобы пользователь не возвращал значения из лямбды.

TMakeGuardHelper написан так, что он принимает лямбда-выражения через оператор. То есть возможна запись TMakeGuardHelper{} | <лямбда-выражение>.

Момент вызова defer-а зависит от места его написания - он вызывается во время выхода из того scope, где он был объявлен, в стандартном для очистки объектов порядке:

{
    A a;
    B b;
    Y_DEFER { <body> };
    C c;
    D d;

    // какой-то код...

    // вызов по очереди d.~D(), c.~C(), { <body> }, b.~B(), a.~A()
}

Код внутри тела defer не должен бросать необработанное исключение, так как это спровоцирует std::terminate.


Clang и LLVM

Про само устройство Clang и LLVM написано уже много статей. На хабре я бы посоветовал эту статью, чтобы понять их краткую историю и общую схему.

В современном мире компиляторы с модульным устройством победили. Clang используется как в больших компаниях - Yandex, Apple, Google (в нём 400-450млн строк кода на C++) и т.д.; так и в больших проектах - FreeBSD, OpenBSD, Android, Chrome, Firefox, LibreOffice и т.д.

Количество стадий компиляций зависит от того, кто объясняет устройство компилятора. Анатомия компилятора многоуровнева и на самом абстрактном уровне выглядит так, что есть три разные программы:

  • Front-end: переводит исходник из C/C++/Ada/Rust/Haskell/... в LLVM IR - особое промежуточное представление. Фронтендом для C-like языков является Clang.

  • Middle-end: LLVM IR оптимизируется в зависимости от настроек.

  • Back-end: LLVM IR переводится в машинный код под нужную платформу - x86/Arm/PowerPC/...

Для простых языков реально написать компилятор под 1000 строк и получить всю мощь фреймворка LLVM - для этого нужно реализовать фронтенд. Также можно использовать lex/yacc - готовые синтаксические парсеры.

На менее абстрактном уровне находится фронтенд Clang, который выполняет такие действия (не рассматривая препроцессор и прочие "микро"-шаги):

  • Лексический анализ: перевод символов в токены, например []() { return 13 + 37; } преобразуются в (l_square) (r_square) (l_paren) (r_paren) (l_brace) (return) (numeric_constant:13) (plus) (numeric_constant:37) (semi) (r_brace).

  • Синтаксический анализ: создание AST (Abstract Syntax Tree), то есть перевод токенов из предыдущего пункта в вид (lambda-expr (body (return-expr (plus-expr (number 13) (number 37))))).

  • Кодогенерация: создание LLVM IR по данному AST.

Таким образом, "области ответственности" очень четко определены, но исходники Clang всё равно гигантские. На мой субъективный взгляд, это связано не столько с распухшим стандартом, сколько с фактом, что C++ - максимально контекстно-зависимый язык.

Стандартом для построения компиляторов считается DragonBook. Clang придерживается его, но C++ слишком сложен, чтобы не заполонить фронтенд ad-hoc проверками и костылями.

Загрузка и сборка Clang

Полная инструкция расположена здесь. Я использую такие команды:

git clone https://github.com/llvm/llvm-project.git
cd llvm-project && mkdir build && cd build
cmake -DLLVM_ENABLE_PROJECTS=clang -DCMAKE_BUILD_TYPE=Release ../llvm
make -j 4

Для пересборки после правки кода можно писать make clang -j 4.

Первый билд будет работать довольно долго. Я выбрал release-сборку -DCMAKE_BUILD_TYPE=Release, так как debug-символов будет столько, что бинарник clang скорее всего не слинкуется (процесс линкера убивается по Out-Of-Memory после пожирания всей оперативки), но вам может повезти с этим больше.

Чтобы бороться с отсутствием debug-символов, для многих объектов можно вызывать метод dump(), который выведет его структуру в stderr. Также, если спровоцировать падение clang-а, то будет выводиться стектрейс вызовов.

Clang использует систему сборки CMake, поэтому можно использовать любой IDE, который умеет его поддерживать.


Новое ключевое слово defer

Если написать слово defer, Clang распознает его как идентификатор (токен вида identifier) в составе выражения (в expression), и код не скомпилируется из-за того, что этот идентификатор нигде не был ранее объявлен (в каком-нибудь declaration).

int main() {
    defer;
}
file.cpp:2:5: error: use of undeclared identifier 'defer'
    defer;
    ^
1 error generated.

Список всех токенов, в том числе ключевых слов, находится в clang/include/clang/Basic/TokenKinds.def. Подобные файлы нужны для того, чтобы разные куски Clang-а могли определять макросы для их обработки и инклюдить их к себе в рандомных местах кода: #include "clang/Basic/TokenKinds.def".

Clang является фронтендом для всех стандартов языков C, C++, Objective-C; для надстроек над языками OpenMP, OpenCL, CUDA и пр.; и для различных костылей и расширений в язык от Microsoft, GNU, самого Clang и пр.

Поэтому значительная часть логики Clang является общей. Если в файле содержится логика для конкретного языка, это отображено в названии: ParseExprCXX.cpp, ParseOpenMP.cpp. Список токенов - общий для всех. Добавим туда новое ключевое слово для C++:

KEYWORD(defer                       , KEYCXX)

И скомпилируем Clang. К счастью, этого достаточно, чтобы лексер (лексический анализатор) научился разбирать его сразу. Если бы мы добавляли что-то наподобии spaceship operator, то пришлось бы дописать код в лексер: коммит со spaceship.

Теперь Clang не думает, что defer это какой-то identifier, но и не понимает, что за выражение перед ним находится:

file.cpp:2:5: error: expected expression
    defer;
    ^
1 error generated.

Это непонимание появляется в парсере (синтаксическом анализаторе) в Parser::ParseCastExpression.

Стектрейс к этому месту
 #3 0x000056335ad3d395 clang::Parser::ParseCastExpression(clang::Parser::CastParseKind, bool, bool&, clang::Parser::TypeCastState, bool, bool*) (/home/izaron/hack/llvm-project/build/bin/clang-14+0x54f8395)
 #4 0x000056335ad3ff2e clang::Parser::ParseCastExpression(clang::Parser::CastParseKind, bool, clang::Parser::TypeCastState, bool, bool*) (/home/izaron/hack/llvm-project/build/bin/clang-14+0x54faf2e)
 #5 0x000056335ad4011d clang::Parser::ParseAssignmentExpression(clang::Parser::TypeCastState) (/home/izaron/hack/llvm-project/build/bin/clang-14+0x54fb11d)
 #6 0x000056335ad448ed clang::Parser::ParseExpression(clang::Parser::TypeCastState) (/home/izaron/hack/llvm-project/build/bin/clang-14+0x54ff8ed)
 #7 0x000056335adbac80 clang::Parser::ParseExprStatement(clang::Parser::ParsedStmtContext) (/home/izaron/hack/llvm-project/build/bin/clang-14+0x5575c80)
 #8 0x000056335adaec0e clang::Parser::ParseStatementOrDeclarationAfterAttributes(llvm::SmallVector<clang::Stmt*, 32u>&, clang::Parser::ParsedStmtContext, clang::SourceLocation*, clang::ParsedAttributesWithRange&) (/home/izaron/hack/llvm-project/build/bin/clang-14+0x5569c0e)
 #9 0x000056335adafeca clang::Parser::ParseStatementOrDeclaration(llvm::SmallVector<clang::Stmt*, 32u>&, clang::Parser::ParsedStmtContext, clang::SourceLocation*) (/home/izaron/hack/llvm-project/build/bin/clang-14+0x556aeca)
#10 0x000056335adb0cf1 clang::Parser::ParseCompoundStatementBody(bool) (/home/izaron/hack/llvm-project/build/bin/clang-14+0x556bcf1)
#11 0x000056335adb1862 clang::Parser::ParseFunctionStatementBody(clang::Decl*, clang::Parser::ParseScope&) (/home/izaron/hack/llvm-project/build/bin/clang-14+0x556c862)
#12 0x000056335ace6997 clang::Parser::ParseFunctionDefinition(clang::ParsingDeclarator&, clang::Parser::ParsedTemplateInfo const&, clang::Parser::LateParsedAttrList*) (/home/izaron/hack/llvm-project/build/bin/clang-14+0x54a1997)
#13 0x000056335ad163af clang::Parser::ParseDeclGroup(clang::ParsingDeclSpec&, clang::DeclaratorContext, clang::SourceLocation*, clang::Parser::ForRangeInit*) (/home/izaron/hack/llvm-project/build/bin/clang-14+0x54d13af)
#14 0x000056335ace12fa clang::Parser::ParseDeclOrFunctionDefInternal(clang::ParsedAttributesWithRange&, clang::ParsingDeclSpec&, clang::AccessSpecifier) (/home/izaron/hack/llvm-project/build/bin/clang-14+0x549c2fa)
#15 0x000056335ace1965 clang::Parser::ParseDeclarationOrFunctionDefinition(clang::ParsedAttributesWithRange&, clang::ParsingDeclSpec*, clang::AccessSpecifier) (.part.0) Parser.cpp:0:0
#16 0x000056335ace9543 clang::Parser::ParseExternalDeclaration(clang::ParsedAttributesWithRange&, clang::ParsingDeclSpec*) (/home/izaron/hack/llvm-project/build/bin/clang-14+0x54a4543)
#17 0x000056335acea87d clang::Parser::ParseTopLevelDecl(clang::OpaquePtr<clang::DeclGroupRef>&, bool) (/home/izaron/hack/llvm-project/build/bin/clang-14+0x54a587d)
#18 0x000056335aceae19 clang::Parser::ParseFirstTopLevelDecl(clang::OpaquePtr<clang::DeclGroupRef>&) (/home/izaron/hack/llvm-project/build/bin/clang-14+0x54a5e19)
#19 0x000056335acdb5ca clang::ParseAST(clang::Sema&, bool, bool) (/home/izaron/hack/llvm-project/build/bin/clang-14+0x54965ca)
#20 0x000056335a018636 clang::CodeGenAction::ExecuteAction() (/home/izaron/hack/llvm-project/build/bin/clang-14+0x47d3636)
#21 0x000056335990bd91 clang::FrontendAction::Execute() (/home/izaron/hack/llvm-project/build/bin/clang-14+0x40c6d91)
#22 0x000056335989c9db clang::CompilerInstance::ExecuteAction(clang::FrontendAction&) (/home/izaron/hack/llvm-project/build/bin/clang-14+0x40579db)
#23 0x00005633599e6070 clang::ExecuteCompilerInvocation(clang::CompilerInstance*) (/home/izaron/hack/llvm-project/build/bin/clang-14+0x41a1070)
#24 0x0000563356a35f44 cc1_main(llvm::ArrayRef<char const*>, char const*, void*) (/home/izaron/hack/llvm-project/build/bin/clang-14+0x11f0f44)
#25 0x0000563356a3311b ExecuteCC1Tool(llvm::SmallVectorImpl<char const*>&) driver.cpp:0:0
#26 0x0000563356959dd0 main (/home/izaron/hack/llvm-project/build/bin/clang-14+0x1114dd0)
#27 0x00007f3492a290b3 __libc_start_main /build/glibc-eX1tMB/glibc-2.31/csu/../csu/libc-start.c:342:3
#28 0x0000563356a32cbe _start (/home/izaron/hack/llvm-project/build/bin/clang-14+0x11edcbe)

На этом месте можно придать defer какой-нибудь смысл, например идентичный ключевому слову new, написав case tok::kw_defer: после case tok::kw_new::

  case tok::kw_new: // [C++] new-expression
  case tok::kw_defer: // [hacked C++] defer-expression
    if (NotPrimaryExpression)
      *NotPrimaryExpression = true;
    Res = ParseCXXNewExpression(false, Tok.getLocation());
    AllowSuffix = false;
    break;

Это позволит компилироваться коду наподобии:

int main() {
    int* i = defer int[13];
    delete[] i;
}

Теперь напишем код, который при встрече defer распарсит составное выражение (compound statement) вслед за ним, сдампит его AST в stderr и выведет warning о том, что defer-выражение было проигнорировано.

Напишем в clang/include/clang/Basic/DiagnosticParseKinds.td наш новый warning:

def warn_unimplemented_defer :
   Warning<"defer statements are not implemented yet, ignore it">;

И в методе Parser::ParseCastExpression:

  case tok::kw_defer: {
    SourceLocation DeferLoc = ConsumeToken(); // skip "defer" token
    StmtResult DeferredStmt(ParseCompoundStatementBody());
    if (!DeferredStmt.isInvalid()) {
        DeferredStmt.get()->dump();
    }

    Diag(DeferLoc, diag::warn_unimplemented_defer);
    return ExprError();
  }

Если по автокомплиту перейти к соответствующим методам и почитать комментарии к ним, то станет понятно, что парсер продвинется вперед на 1 токен (т.е. пропустит слово defer), потом распарсит составное выражение (вида { ... }), и также пропустит его, выведет в stderr его структуру, и добавит новый warning под словом defer.

Пересоберем Clang еще раз. Для диагностических сообщений есть кодогенерация, написанная непосредственно в конфиге CMake, поэтому если Clang "не увидит" новую диагностику, перед билдом придется запустить еще раз cmake -DLLVM_ENABLE_PROJECTS=clang -DCMAKE_BUILD_TYPE=Release ../llvm.

Запустим Clang на более сложном примере:

#include <cstdio>
int main() {
    defer {
        int var = 1 + 2;
        printf("1 + 2 is %d\n", var);
    };
    printf("hello world!\n");
}
izaron@izaron:~/check$ ~/hack/llvm-project/build/bin/clang++ file.cpp 
CompoundStmt 0x560115cc6e80
|-DeclStmt 0x560115cc6c68
| `-VarDecl 0x560115cc6ba0  used var 'int' cinit
|   `-BinaryOperator 0x560115cc6c48 'int' '+'
|     |-IntegerLiteral 0x560115cc6c08 'int' 1
|     `-IntegerLiteral 0x560115cc6c28 'int' 2
`-CallExpr 0x560115cc6e20 'int'
  |-ImplicitCastExpr 0x560115cc6e08 'int (*)(const char *__restrict, ...)' <FunctionToPointerDecay>
  | `-DeclRefExpr 0x560115cc6d90 'int (const char *__restrict, ...)' lvalue Function 0x560115cac748 'printf' 'int (const char *__restrict, ...)'
  |-ImplicitCastExpr 0x560115cc6e50 'const char *' <ArrayToPointerDecay>
  | `-StringLiteral 0x560115cc6d48 'const char [13]' lvalue "1 + 2 is %d\n"
  `-ImplicitCastExpr 0x560115cc6e68 'int' <LValueToRValue>
    `-DeclRefExpr 0x560115cc6d70 'int' lvalue Var 0x560115cc6ba0 'var' 'int'
file.cpp:3:5: warning: defer statements are not implemented yet, ignore it
    defer {
    ^
1 warning generated.
izaron@izaron:~/check$ ./a.out 
hello world!

Примечание: когда нужно, printf используется вместо std::cout для более понятных AST.


Как реализовать defer?

У нас есть новое ключевое слово, но оно пока не делает то, что нам нужно. Надо рассмотреть несколько фич, которые помогут нам определиться с выбором подхода к реализации.

Атрибут cleanup

Про этот атрибут можно почитать в этой статье. К автоматической переменной (любого типа) можно "прицепить" функцию, которая как аргумент принимает ссылку на эту переменную и будет вызываться в момент выхода из scope.

#include <iostream>

struct dummy_t {
    dummy_t() {
        std::cout << "on constructor for obj " << this << std::endl;
    }
    ~dummy_t() {
        std::cout << "on destructor for obj " << this << std::endl;
    }
};

void on_cleanup(dummy_t* dummy) {
    std::cout << "on cleanup function for obj " << dummy << std::endl;
}

int main() {
    std::cout << "before scope" << std::endl;
    {
        __attribute__((cleanup(on_cleanup))) dummy_t dummy;
        std::cout << "inside scope" << std::endl;
    }
    std::cout << "after scope" << std::endl;
}
before scope
on constructor for obj 0x7fff060964b8
inside scope
on cleanup function for obj 0x7fff060964b8
on destructor for obj 0x7fff060964b8
after scope

После синтаксического анализа видим метод on_cleanup:

TranslationUnitDecl 0x205f068 <<invalid sloc>> <invalid sloc>
|-FunctionDecl 0x2ac5ac8 <line:12:1, line:14:1> line:12:6 used on_cleanup 'void (dummy_t *)'
| |-ParmVarDecl 0x2ac5a08 <col:17, col:26> col:26 used dummy 'dummy_t *'
| `-CompoundStmt 0x2ac8fc8 <col:33, line:14:1>
...

И на variable declaration кроме неявного вызова конструктора еще повешен атрибут CleanupAttr со ссылкой на метод on_cleanup:

    | |-DeclStmt 0x2acba70 <line:19:9, col:59>
    | | `-VarDecl 0x2acb7d0 <col:9, col:54> col:54 dummy 'dummy_t' callinit destroyed
    | |   |-CXXConstructExpr 0x2acba48 <col:54> 'dummy_t' 'void ()'
    | |   `-CleanupAttr 0x2acb838 <col:24, col:42> Function 0x2ac5ac8 'on_cleanup' 'void (dummy_t *)'

Вызов нужного метода со всеми проверками прописывается во время кодогенерации из AST в LLVM IR в этом месте. В исходниках Clang есть понятие "cleanup" - кодогенерация для вещей по типу деструкторов и CleanupAttr. Cleanup-ы организованы в виде LIFO-стека, и при выходе из scope, N последних cleanup-ов производятся в обратном порядке.

Для имитации defer-а эта конструкция не очень подходит, потому что потребуется зарегистрировать метод-болванку для defer-выражения и создать переменную-болванку, на которой будет висеть CleanupAttr.

Лямбда-выражения

Если какой-то функциональности в языке нет изначально, и на ее реализацию не сильно закладывались при дизайне, то велик шанс того, что при ее добавлении будут использоваться костыли (скрывая их за эвфемизмами) на основе уже существующих идиом. Не обошлось без них при введении лямбда-выражений в С++11.

В C++ исторически нет и не было такой возможности, что внутри метода можно написать другой метод, и использовать его, как в Python:

def sample():
    def sum(a, b):
        return a + b
    print(sum(1, 2))
sample()

Хотя, как ни странно, объявлять class, struct, enum, union (это близкородственные сущности) можно почти везде, в том числе внутри методов.

Есть статья про историю лямбд, где описано, как прото-лямбды существовали уже в C++03, и сегодняшние лямбда-выражения используют тот же подход, скрывая его за чертогами компилятора.

Прото-лямбда для [](int x) { std::cout << x << std::endl; }
#include <iostream>
#include <algorithm>
#include <vector>

struct PrintFunctor {
    void operator()(int x) const {
        std::cout << x << std::endl;
    }
};

int main() {
    std::vector<int> v;
    v.push_back(1);
    v.push_back(2);
    std::for_each(v.begin(), v.end(), PrintFunctor());   
}

Таким образом, посмотрев AST для лямбда-выражения, можно увидеть скрытый от посторонних глаз класс (он же CXXRecordDecl):

Пример AST для лямбда-выражения
#include <cstdio>
int main() {
    int gamma = 3;
    const auto l = [&](int alpha, int beta) { printf("%d\n", alpha + beta + gamma); };
    l(1, 2);
}
`-FunctionDecl 0x24bbc38 <lambda.cpp:2:1, line:6:1> line:2:5 main 'int ()'
  `-CompoundStmt 0x24beb38 <col:12, line:6:1>
    |-DeclStmt 0x24bbd78 <line:3:5, col:18>
    | `-VarDecl 0x24bbcf0 <col:5, col:17> col:9 used gamma 'int' cinit
    |   `-IntegerLiteral 0x24bbd58 <col:17> 'int' 3
    |-DeclStmt 0x24be9c0 <line:4:5, col:86>
    | `-VarDecl 0x24bbdf0 <col:5, col:85> col:16 used l 'const (lambda at lambda.cpp:4:20)':'const (lambda at lambda.cpp:4:20)' cinit
    |   `-ExprWithCleanups 0x24be9a8 <col:20, col:85> 'const (lambda at lambda.cpp:4:20)':'const (lambda at lambda.cpp:4:20)'
    |     `-CXXConstructExpr 0x24be978 <col:20, col:85> 'const (lambda at lambda.cpp:4:20)':'const (lambda at lambda.cpp:4:20)' 'void ((lambda at lambda.cpp:4:20) &&) noexcept' elidable
    |       `-MaterializeTemporaryExpr 0x24be848 <col:20, col:85> '(lambda at lambda.cpp:4:20)' xvalue
    |         `-LambdaExpr 0x24be268 <col:20, col:85> '(lambda at lambda.cpp:4:20)'
    |           |-CXXRecordDecl 0x24bdc00 <col:20> col:20 implicit class definition
    |           | |-DefinitionData lambda pass_in_registers trivially_copyable can_const_default_init
    |           | | |-DefaultConstructor
    |           | | |-CopyConstructor simple trivial has_const_param implicit_has_const_param
    |           | | |-MoveConstructor exists simple trivial
    |           | | |-CopyAssignment trivial has_const_param needs_implicit implicit_has_const_param
    |           | | |-MoveAssignment
    |           | | `-Destructor simple irrelevant trivial
    |           | |-CXXMethodDecl 0x24bdd40 <col:43, col:85> col:20 used operator() 'void (int, int) const' inline
    |           | | |-ParmVarDecl 0x24bbe70 <col:24, col:28> col:28 used alpha 'int'
    |           | | |-ParmVarDecl 0x24bbef0 <col:35, col:39> col:39 used beta 'int'
    |           | | `-CompoundStmt 0x24be098 <col:45, col:85>
    |           | |   `-CallExpr 0x24be050 <col:47, col:82> 'int'
    |           | |     |-ImplicitCastExpr 0x24be038 <col:47> 'int (*)(const char *__restrict, ...)' <FunctionToPointerDecay>
    |           | |     | `-DeclRefExpr 0x24bdfc0 <col:47> 'int (const char *__restrict, ...)' lvalue Function 0x249f198 'printf' 'int (const char *__restrict, ...)'
    |           | |     |-ImplicitCastExpr 0x24be080 <col:54> 'const char *' <ArrayToPointerDecay>
    |           | |     | `-StringLiteral 0x24bde88 <col:54> 'const char [4]' lvalue "%d\n"
    |           | |     `-BinaryOperator 0x24bdfa0 <col:62, col:77> 'int' '+'
    |           | |       |-BinaryOperator 0x24bdf18 <col:62, col:70> 'int' '+'
    |           | |       | |-ImplicitCastExpr 0x24bdee8 <col:62> 'int' <LValueToRValue>
    |           | |       | | `-DeclRefExpr 0x24bdea8 <col:62> 'int' lvalue ParmVar 0x24bbe70 'alpha' 'int'
    |           | |       | `-ImplicitCastExpr 0x24bdf00 <col:70> 'int' <LValueToRValue>
    |           | |       |   `-DeclRefExpr 0x24bdec8 <col:70> 'int' lvalue ParmVar 0x24bbef0 'beta' 'int'
    |           | |       `-ImplicitCastExpr 0x24bdf88 <col:77> 'int' <LValueToRValue>
    |           | |         `-DeclRefExpr 0x24bdf68 <col:77> 'int' lvalue Var 0x24bbcf0 'gamma' 'int'
    |           | |-FieldDecl 0x24be200 <col:77> col:77 implicit referenced 'int &'
    |           | |-CXXDestructorDecl 0x24be2b0 <col:20> col:20 implicit referenced ~ 'void () noexcept' inline default trivial
    |           | |-CXXConstructorDecl 0x24be510 <col:20> col:20 implicit constexpr  'void (const (lambda at lambda.cpp:4:20) &)' inline default trivial noexcept-unevaluated 0x24be510
    |           | | `-ParmVarDecl 0x24be628 <col:20> col:20 'const (lambda at lambda.cpp:4:20) &'
    |           | `-CXXConstructorDecl 0x24be6c8 <col:20> col:20 implicit used constexpr  'void ((lambda at lambda.cpp:4:20) &&) noexcept' inline default trivial
    |           |   |-ParmVarDecl 0x24be7d8 <col:20> col:20 used '(lambda at lambda.cpp:4:20) &&'
    |           |   |-CXXCtorInitializer Field 0x24be200 '' 'int &'
    |           |   | `-MemberExpr 0x24be8f0 <col:20> 'int' lvalue . 0x24be200
    |           |   |   `-CXXStaticCastExpr 0x24be8c0 <col:20> '(lambda at lambda.cpp:4:20)' xvalue static_cast<class (lambda at lambda.cpp:4:20) &&> <NoOp>
    |           |   |     `-DeclRefExpr 0x24be890 <col:20> '(lambda at lambda.cpp:4:20)' lvalue ParmVar 0x24be7d8 '' '(lambda at lambda.cpp:4:20) &&'
    |           |   `-CompoundStmt 0x24be968 <col:20>
    |           |-DeclRefExpr 0x24be1c8 <col:21> 'int' lvalue Var 0x24bbcf0 'gamma' 'int'
    |           `-CompoundStmt 0x24be098 <col:45, col:85>
    |             `-CallExpr 0x24be050 <col:47, col:82> 'int'
    |               |-ImplicitCastExpr 0x24be038 <col:47> 'int (*)(const char *__restrict, ...)' <FunctionToPointerDecay>
    |               | `-DeclRefExpr 0x24bdfc0 <col:47> 'int (const char *__restrict, ...)' lvalue Function 0x249f198 'printf' 'int (const char *__restrict, ...)'
    |               |-ImplicitCastExpr 0x24be080 <col:54> 'const char *' <ArrayToPointerDecay>
    |               | `-StringLiteral 0x24bde88 <col:54> 'const char [4]' lvalue "%d\n"
    |               `-BinaryOperator 0x24bdfa0 <col:62, col:77> 'int' '+'
    |                 |-BinaryOperator 0x24bdf18 <col:62, col:70> 'int' '+'
    |                 | |-ImplicitCastExpr 0x24bdee8 <col:62> 'int' <LValueToRValue>
    |                 | | `-DeclRefExpr 0x24bdea8 <col:62> 'int' lvalue ParmVar 0x24bbe70 'alpha' 'int'
    |                 | `-ImplicitCastExpr 0x24bdf00 <col:70> 'int' <LValueToRValue>
    |                 |   `-DeclRefExpr 0x24bdec8 <col:70> 'int' lvalue ParmVar 0x24bbef0 'beta' 'int'
    |                 `-ImplicitCastExpr 0x24bdf88 <col:77> 'int' <LValueToRValue>
    |                   `-DeclRefExpr 0x24bdf68 <col:77> 'int' lvalue Var 0x24bbcf0 'gamma' 'int'
    `-CXXOperatorCallExpr 0x24beaf8 <line:5:5, col:11> 'void':'void'
      |-ImplicitCastExpr 0x24beab8 <col:6, col:11> 'void (*)(int, int) const' <FunctionToPointerDecay>
      | `-DeclRefExpr 0x24bea38 <col:6, col:11> 'void (int, int) const' lvalue CXXMethod 0x24bdd40 'operator()' 'void (int, int) const'
      |-DeclRefExpr 0x24be9d8 <col:5> 'const (lambda at lambda.cpp:4:20)':'const (lambda at lambda.cpp:4:20)' lvalue Var 0x24bbdf0 'l' 'const (lambda at lambda.cpp:4:20)':'const (lambda at lambda.cpp:4:20)'
      |-IntegerLiteral 0x24be9f8 <col:7> 'int' 1
      `-IntegerLiteral 0x24bea18 <col:10> 'int' 2

На cppreference есть хорошее описание lambda expression, где описывается устройство создаваемого класса. При этом описание может сбить с толку неподготовленного читателя:

The lambda expression is a prvalue expression of unique unnamed non-union non-aggregate class type

Дело в том, что во время костылизации новой фичи, внутри компилятора можно делать вещи, которые решительно запрещены или отсутствуют в стандарте - например, создавать нетривиальные конструкторы/деструкторы безымянным классам. В стандарте невозможно прописать это внятным образом, поэтому у многих возникает представление о лямбдах и прочих нетривиальных вещах как о "черном ящике".

Таким образом, для имитации defer вполне подошел бы путь лямбда-выражений: создавать объект "магического" класса, который выполнит некие действия в деструкторе. При этом не надо создаваться новые сущности (как в случае с cleanup-аттрибутом: новый атрибут для AST).

Poor man's defer

defer-ы в production C++ коде в том формате, про который мы говорим, сейчас практически всегда выглядят так:

    struct dummy_t {
        ~dummy_t() {
            // какой-то код
        }
    } dummy;

Тут много лишнего: структура не безымянная, а имеет имя, и к тому же для этой структуры создаётся объект. Эти названия, конечно, выкинутся оптимизатором, и объект не станет занимать память на стеке, но лучше бы, чтобы их не было.

Выбранный подход

В учебном примере defer будет скрывать под собой объявление struct таким образом: объявление

defer {
    <compound-expression>
};

Будет работать идентично объявлению

struct <MAGIC-STRUCT-NAME> {
    ~<MAGIC-STRUCT-NAME>() {
        <compound-expression>
    }
} <MAGIC-OBJECT-NAME>;

При этом желательно либо совсем убрать из скоупа идентификаторы <MAGIC-STRUCT-NAME> и <MAGIC-OBJECT-NAME> (с компиляторскими фокусами как для лямбда-выражений), либо дать им уникальные имена, чтобы было возможно иметь несколько defer-ов в одном скоупе.


Создаем defer

Полный коммит доступен по этой ссылке.

defer как анонимный struct

В исходниках Clang в парсере разделяется обработка объявлений (ParseDecl.cpp/ParseDeclCXX.cpp) и выражений (ParseExpr.cpp/ParseExprCXX.cpp).

"По умолчанию" считается, что мы парсим выражение, поэтому чтобы defer начали принимать за объявление (причём за объявление структуры), нужно добавить кое-где условия:

В Parser::isCXXDeclarationSpecifier и в Parser::ParseDeclarationSpecifiers добавим case tok::kw_defer:.

В методе Parser::ParseClassSpecifier делаем defer аналогичным struct:

  const bool IsDefer = TagTokKind == tok::kw_defer;
  if (TagTokKind == tok::kw_struct || IsDefer)
    TagType = DeclSpec::TST_struct;

В реальном коде, скорее всего, defer лучше было бы сделать отдельным типом class-specifier (DeclSpec::TST_struct), но в учебных целях не будем заморачиваться с дизайном, потому что будет очень много копипаста.

Чуть ниже в этом же методе происходит парсинг имени класса (IdentifierInfo *Name). Запретим объявлять имена у defer-структуры:

def err_named_defer_definition : Error<"defer structs should not have name">;
    // In case of "defer" it should not have name
    if (IsDefer) {
      if (Name != nullptr) {
        Diag(NameLoc, diag::err_named_defer_definition);
      }
    }

Вывод компилятора при нарушении этого правила:

izaron@izaron:~/check$ ~/hack/llvm-project/build/bin/clang++ test.cpp 
test.cpp:3:11: error: defer structs should not have name
    defer defer_t { };
          ^
1 error generated.

Имя для struct "из воздуха"

Теперь, если захочется дать структуре какое-то имя, которое юзер не писал в исходниках (другими словами, самовольно сделать из struct { ... } объявление struct defer012345 { ... }), можно столкнуться с тем, что неясно, как это сделать.

Все названия (для классов, переменных и т.д.) берутся из токенов-идентификаторов, которые были получены на стадии лексического анализа, и вроде как неоткуда достать какой-то новый. Но рамки не настолько жёсткие: есть законный хак для регистрации своего идентификатора в таблице идентификаторов.

  // If this is a defer struct, create custom name for it
  // "defer {" should imitate "struct defer012345 {"
  if (IsDefer) {
    Name = &Context.Idents.getOwn("defer012345");
  }

Если бы мы захотели, можно было бы самовольно присвоить defer-структуре уникальное имя в зависимости от SourceLocation (положения defer-а в исходнике), как сделано в Y_DEFER в начале статьи.

Деструктор для анонимного struct

Однако предыдущий пункт мы не будем использовать, так как анонимной структуре можно присвоить деструктор, и это будет работать.

Если посмотреть, как парсятся деструкторы в "обычных" условиях, видно, что это нетривиальное дело. Это происходит в два прохода. В первый проход распарсится unqualified-id (понятие из стандарта), который являет собой запись ~classname().

Стектрейс парсинга до метода getDestructorName
 #3 0x000056503ce39c00 clang::Parser::ParseUnqualifiedId(clang::CXXScopeSpec&, clang::OpaquePtr<clang::QualType>, bool, bool, bool, bool, bool, clang::SourceLocation*, clang::UnqualifiedId&) (/home/izaron/hack/llvm-project/build/bin/clang-14+0x5515c00)
 #4 0x000056503cdfd200 clang::Parser::ParseDirectDeclarator(clang::Declarator&) (/home/izaron/hack/llvm-project/build/bin/clang-14+0x54d9200)
 #5 0x000056503cde36d0 clang::Parser::ParseDeclaratorInternal(clang::Declarator&, void (clang::Parser::*)(clang::Declarator&)) (/home/izaron/hack/llvm-project/build/bin/clang-14+0x54bf6d0)
 #6 0x000056503ce0dd4d clang::Parser::ParseCXXMemberDeclaratorBeforeInitializer(clang::Declarator&, clang::VirtSpecifiers&, clang::ActionResult<clang::Expr*, true>&, clang::Parser::LateParsedAttrList&) (/home/izaron/hack/llvm-project/build/bin/clang-14+0x54e9d4d)
 #7 0x000056503ce10bd1 clang::Parser::ParseCXXClassMemberDeclaration(clang::AccessSpecifier, clang::ParsedAttributes&, clang::Parser::ParsedTemplateInfo const&, clang::ParsingDeclRAIIObject*) (/home/izaron/hack/llvm-project/build/bin/clang-14+0x54ecbd1)
 #8 0x000056503ce1381c clang::Parser::ParseCXXClassMemberDeclarationWithPragmas(clang::AccessSpecifier&, clang::ParsedAttributesWithRange&, clang::TypeSpecifierType, clang::Decl*) (/home/izaron/hack/llvm-project/build/bin/clang-14+0x54ef81c)
 #9 0x000056503ce13e2b clang::Parser::ParseCXXMemberSpecification(clang::SourceLocation, clang::SourceLocation, clang::ParsedAttributesWithRange&, unsigned int, clang::Decl*) (/home/izaron/hack/llvm-project/build/bin/clang-14+0x54efe2b)
#10 0x000056503ce15fde clang::Parser::ParseClassSpecifier(clang::tok::TokenKind, clang::SourceLocation, clang::DeclSpec&, clang::Parser::ParsedTemplateInfo const&, clang::AccessSpecifier, bool, clang::Parser::DeclSpecContext, clang::ParsedAttributesWithRange&) (/home/izaron/hack/llvm-project/build/bin/clang-14+0x54f1fde)

В этом коде довольно сложно разбираться, и сами разработчики Clang жалуются на то, что стандарт запутывает.

Как бы то ни было, после того, как деструктор зарегистрирован, он является unevaluated (понятие из Clang), и вторым проходом является определение тела метода.

Определение тел методов класса откладывается до того момента, как не будут зарегистрированы все поля и методы класса. Видимо, для того, чтобы внутри методов компилятор понимал обращения к другим методам и полям этого класса.

Мы будем имитировать эти два прохода - зарегистрируем деструктор, и подсунем токены для определения его тела.

Для регистрации деструктора нам поможет метод Sema::DeclareImplicitDestructor, который строит неявный деструктор. Мы сделаем похожий Sema::DeclareUserDestructor, который зарегистрирует заготовку user-defined деструктора. Почти всё там - копипаст из исходного метода, но мы пока не заморачиваемся с дизайном:

Определение Sema::DeclareUserDestructor
CXXDestructorDecl *Sema::DeclareUserDestructor(CXXRecordDecl *ClassDecl) {
  DeclaringSpecialMember DSM(*this, ClassDecl, CXXDestructor);
  if (DSM.isAlreadyBeingDeclared())
    return nullptr;

  // Create the actual destructor declaration.
  CanQualType ClassType
    = Context.getCanonicalType(Context.getTypeDeclType(ClassDecl));
  SourceLocation ClassLoc = ClassDecl->getLocation();
  DeclarationName Name
    = Context.DeclarationNames.getCXXDestructorName(ClassType);
  DeclarationNameInfo NameInfo(Name, ClassLoc);
  CXXDestructorDecl *Destructor = CXXDestructorDecl::Create(
      Context, ClassDecl, ClassLoc, NameInfo, QualType(), nullptr,
      getCurFPFeatures().isFPConstrained(),
      /*isInline=*/false,
      /*isImplicitlyDeclared=*/false,
      ConstexprSpecKind::Unspecified);
  Destructor->setAccess(AS_public);

  setupImplicitSpecialMemberType(Destructor, Context.VoidTy, None);

  Destructor->setTrivial(false);

  ++getASTContext().NumImplicitDestructorsDeclared;

  Scope *S = getScopeForContext(ClassDecl);
  CheckImplicitSpecialMemberDeclaration(S, Destructor);

  // Introduce this destructor into its scope.
  if (S)
    PushOnScopeChains(Destructor, S, false);
  ClassDecl->addDecl(Destructor);

  return Destructor;
}

Теперь можно зайти в метод Parser::ParseCXXMemberSpecification. Сделаем, чтобы он принимал еще один аргумент bool IsDefer, и после этой строки произведём оба прохода:

Новый код в Parser::ParseCXXMemberSpecification
  // defer structs should have only destructor definition instead of whole class body
  if (TagDecl && IsDefer) {
    Actions.ActOnStartCXXMemberDeclarations(getCurScope(), TagDecl, FinalLoc,
                                            IsFinalSpelledSealed, IsAbstract,
                                            TagDecl->getBeginLoc());

    // First pass - register user-defined destructor
    CXXRecordDecl* ClassDecl = dyn_cast<CXXRecordDecl>(TagDecl);
    CXXDestructorDecl* ClassDestructor = Actions.DeclareUserDestructor(ClassDecl);

    // Second pass - add "late parsed" destructor body declaration
    // I didn't found the method to get all tokens from "{ ... }", so I wrote the algo by hand
    LexedMethod* LM = new LexedMethod(this, ClassDestructor);
    getCurrentClass().LateParsedDeclarations.push_back(LM);
    CachedTokens& Toks = LM->Toks;

    Toks.push_back(Tok);
    unsigned tokenIndex = 1;
    unsigned bracesNum = 1;
    while (bracesNum > 0) {
        const Token& t = GetLookAheadToken(tokenIndex);
        Toks.push_back(t);
        if (t.is(tok::l_brace)) {
            ++bracesNum;
        } else if (t.is(tok::r_brace)) {
            --bracesNum;
        }
        ++tokenIndex;
    }

    // Finish defer struct definition
    ParsedAttributes attrs(AttrFactory);
    Actions.ActOnFinishCXXMemberSpecification(getCurScope(), RecordLoc, TagDecl,
                                              TagDecl->getBeginLoc(),
                                              TagDecl->getEndLoc(), attrs);

    ParseLexedMethodDefs(getCurrentClass());

    Actions.ActOnTagFinishDefinition(getCurScope(), TagDecl, SourceRange(TagDecl->getBeginLoc(), TagDecl->getEndLoc()));

    // Leave the class scope.
    ParsingDef.Pop();
    ClassScope.Exit();

    // Skip defer body
    BalancedDelimiterTracker T(*this, tok::l_brace);
    T.consumeOpen();
    T.skipToEnd();

    return;
  }

Здесь регистрируется деструктор, и подсовываются токены, которые планируются быть его телом. Я не нашел метода, который отдаст мне все токены между открывающей скобкой { и закрывающей }, поэтому добавил их "руками". Затем указатель на текущий токен сдвигается и начинает указывать на следующий токен после }. Никаких других полей и методов у defer-структуры не будет.

Все остальные методы из этого куска кода нужны, чтобы класс нормально зарегистрировался, и скопипасчены из этого же метода. В разработке так делать не надо - я специально делаю сомнительные решения по дизайну, чтобы оставалось понятным, что мы делаем.

Скомпилируем этот код и запустим его:

#include <cstdio>

int main() {
    defer { printf("deferred 0\n"); } d0;

    defer {
        defer { printf("inner deferred\n"); } d;
        printf("deferred 1\n");
    } d1;

    printf("hello!\n");
    defer { printf("deferred 2\n"); } d2;
    printf("world!\n");
}
hello!
world!
deferred 2
deferred 1
inner deferred
deferred 0

Посмотрим, что же за деструкторы вызываются в LLVM IR, потому что на уровне AST они действительно безымянные. Запускаем ~/hack/llvm-project/build/bin/clang++ -emit-llvm -S test.cpp, открываем test.ll, и видим подобные вызовы:

call void @"_ZZ4mainEN3$_0D2Ev"(%struct.anon.2* nonnull align 1 dereferenceable(1) %5) #4

С названиями методов происходит name mangling. Произведем demangle имён:

main::$_0::~$_0()
main::$_1::~$_1()
main::$_2::~$_2()
main::$_1::~$_1()::$_3::~$_3()

Названиям анонимных структур все-таки присвоились имена, но на позднем этапе - вызовы по типу (*this).~$_0() и d0.~() не будут компилироваться.

Анонимные переменные-"болванки"

В выражениях defer { ... } d; переменная d является "болванкой", нужной только потому, что без нее defer-структура не получит объект, у которого будет вызываться деструктор. Однако переменные тоже можно сделать "анонимными".

Если не объявлять болванку, то есть написать struct { ... };/defer { ... };, то выведется несколько ошибок, и будет создана "битая" болванка:

AST выражения struct{};
    `-DeclStmt 0x559d6c6c98d8 <line:17:5, col:14>
      |-CXXRecordDecl 0x559d6c6c96d0 <col:5, col:13> col:5 struct definition
      | `-DefinitionData is_anonymous pass_in_registers empty aggregate standard_layout trivially_copyable pod trivial literal has_constexpr_non_copy_move_ctor can_const_default_init
      |   |-DefaultConstructor exists trivial constexpr needs_implicit defaulted_is_constexpr
      |   |-CopyConstructor simple trivial has_const_param needs_implicit implicit_has_const_param
      |   |-MoveConstructor exists simple trivial needs_implicit
      |   |-CopyAssignment simple trivial has_const_param needs_implicit implicit_has_const_param
      |   |-MoveAssignment exists simple trivial needs_implicit
      |   `-Destructor simple irrelevant trivial needs_implicit
      `-VarDecl 0x559d6c6c9858 <col:5> col:5 implicit invalid '(anonymous struct at test.cpp:17:5)'

Подебажив Clang, можно увидеть, что "битая" болванка создаётся в Parser::ParseSimpleDeclaration.

В самом начале этого метода зафиксируем факт наличие defer-а:

  const bool IsDefer = Tok.is(tok::kw_defer);

После парсинга класса и перед проверкой на tok::semi (это ;) запретим defer-структурам иметь что-то после закрывающей скобки }:

  if (IsDefer && !Tok.is(tok::semi)) {
    Diag(Tok.getLocation(), diag::err_named_defer_variable);
    return nullptr;
  }
def err_named_defer_variable : Error<"defer structs should not have variables">;

В метод ParsedFreeStandingDeclSpec и дальше до метода BuildAnonymousStructOrUnion мы прокинем флаг IsDefer и сделаем так, чтобы для defer создавалась нормальная болванка - безымянный объект безымянной структуры. (Что именно поменялось, можно увидеть в ссылке на коммит).

Теперь успешно компилируется и запускается этот код:

#include <iostream>
using std::cout;
using std::endl;

int main() {
    defer { cout << "deferred 0" << endl; };

    defer {
        defer { cout << "inner deferred" << endl; };
        cout << "deferred 1" << endl;
    };

    cout << "hello" << endl;
    defer { cout << "deferred 2" << endl; };
    cout << "world!" << endl;
}
hello
world!
deferred 2
deferred 1
inner deferred
deferred 0

Захват автоматических переменных

Если объявить класс внутри метода, он не будет "видеть" автоматические переменные, объявленные в этом методе до класса. В самописных defer-ах делается руками (а в лямбда-выражениях компилятором) такие вещи:

Захват автоматических переменных
#include <string>

int main() {
    int alpha;
    double beta;
    std::string gamma;

    class dummy_t {
    public:
        dummy_t(int& alpha, double& beta, std::string& gamma)
            : alpha{alpha}
            , beta{beta}
            , gamma{gamma}
        {}

        ~dummy_t() {
            // some code...
            // "alpha", "beta" and "gamma" are available
        }

    private:
        int& alpha;
        double& beta;
        std::string& gamma;
    } dummy{alpha, beta, gamma};
}

В "нашу" defer-структуру надо добавить самописный конструктор, подсунуть несколько полей, и вызвать его у переменной-"болванки". Это сделать нетривиально, поэтому реализации в рамках этой статьи нету. Но вы можете попробовать это сделать!


Как можно было бы сделать по-другому?

На правах шутки: Так как пример учебный, и defer не сильно нужен в C++, то его можно было бы сделать никак =)

Вопрос реализации имеет две стороны:

Сторона компилятора

Можно было бы не генерировать структуру из defer, а сделать DeferExpr - новую сущность для AST. Тогда во время кодогенерации из AST в LLVM IR можно поместить кусок кода в конец метода. Однако это имеет более высокую сложность реализации.

LLVM IR оперирует "блоками" - это несколько подряд идущих инструкций, которые всегда выполняются один за другим. Блок начинается с метки (label), и заканчивается либо терминирующей инструкцией, либо прыжком в начало другого блока в зависимости от результата проверки.

По-хорошему "последний" блок (который оканчивает выполнение метода) должен быть ровно один, но в реальности их может быть сколько угодно из-за того, что unwinding стека по exception-у обрабатывается одной инструкцией, а "просто" возврат из метода - другим. В принципе работа с блоками это отдельная история, и там есть свои причуды.

Другой путь - если видим defer-блок, можно в AST-дереве "передвинуть" этот блок в конец скоупа, где он был объявлен. Но тогда надо будет исправлять проблему, что defer-блок будет "видеть" автоматические переменные, объявленные после него.

Сторона языка

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

Понятно, что ввод нового ключевого слова может плохо повлиять на программы, где есть одноименные переменные/методы/классы. Есть пара способов снизить урон:

  1. Сделать не keyword, а identifier with special meaning. Например, слова final и override парсятся Clang-ом как токен-идентификатор, и приобретают особый смысл только в некоторых ситуациях, а в прочих случаях могут являться именем переменной или метода.

  2. Использовать атрибуты, например [[defer]] { ... }. Минус в том, что неизвестные компилятору атрибуты игнорируются с warning-ом, а не падают с ошибкой компиляции: warning: unknown attribute 'defer' ignored [-Wunknown-attributes].

  3. Использовать вне-стандартовые аттрибуты: __attribute((defer))__ { ... }. Но минусы такие же, как в прошлом пункте. Их можно использовать, если полагаться на то, что в конкретном компиляторе это расширение есть и всегда будет прилично работать.


Конец

Надеюсь, что во время создания своего ключевого слова вы узнали много интересных вещей про устройство компиляторов!

Adblock test (Why?)

Google начала блокировать почтовые ящики бывших чиновников Афганистана, чтобы они не достались Талибану*

По информации Reuters, Google начала массово блокировать почтовые ящики бывших чиновников Афганистана, чтобы информация из них не досталась Талибану (признана террористической и запрещена в России). Американская компания опасается, что представители новой власти страны получат доступ к конфиденциальным данным и официальной переписке прежнего руководства страны, включая данные и переписку различных министерств и ведомств от министерства финансов, промышленности и высшего образования до горнодобывающей промышленности.
Источник издания пояснил, что сервисы электронной почты Gmail и Google Документы в правительстве использовали очень часто. Часть важных данных там хранила даже афганская служба протокола президента. Также большинство списков сотрудников различных ведомств страны есть в сервисе Google Таблицы.

Reuters пояснило, что помимо доступа к облачным сервисам, Талибан пытается использовать существующие учетные записи для сохранения и копирования данных на серверах
различных министерств. Некоторые их бывшие технические сотрудники противостоят таким действиям и пытаются закрыть этот доступ или самостоятельно удаляют информацию.

После получения доступа к информационным ресурсам у Талибана будут базы данных и электронные письма с персональной информацией о работниках прежнего правительства, подрядчиках, союзниках и зарубежных партнерах, включая их биометрические и платежные
данные.

Google пытается предотвратить попытки Талибана получить эту информацию, чтобы использовать ее против пользователей компании. Google объяснила, что предпринимает временные меры для защиты соответствующих учетных записей.

Reuters уточнило, что часть сотрудников бывших афганских ведомств использовали почтовые сервисы Microsoft. Это делали как минимум министерство иностранных дел и президентское управление. Компания не уточнила изданию, будет ли она защищать эти учетные записи от Талибана.

Эксперт Reuters рассказал, что попытки талибов контролировать созданную США цифровую инфраструктуру заслуживают внимания и должны быть пресечены. По его словам, разведданные, полученные из этой инфраструктуры, могут быть гораздо более ценными для молодого правительства, чем старые вертолеты и другое вооружение, брошенное американской армией.

Талибан признана террористической группировкой в США. Все американские соцсети и сервисы должны блокировать аккаунты ее пользователей, согласно политике страны об опасных организациях.

В середине августа WhatsApp заявила, что не может отправить пользователей организации Талибан в бан или блокировать их публикации. Сотрудники сервиса не понимают, что они пишут и не могут оперативно наложить административные запреты. Тем не менее, аккаунты представителей Талибана сейчас блокируются в Facebook и Instagram. Специалисты Facebook задействовали группу экспертов по афганским языкам и диалектам, включая пушту и дари, которые в режиме реального времени анализируют и удаляют запрещенный контент в соцсетях Facebook и Instagram.

Adblock test (Why?)

Facebook, Twitter и Telegram грозят новые штрафы по 9 протоколам РКН на сумму до 52 млн рублей

Мировой суд Таганского района Москвы 14 сентября рассмотрит 9 административных дел по протоколам Роскомнадзора против Facebook, Twitter и Telegram за неудаление соцсетями запрещенного контента, в том числе за публикации с призывами к подросткам выйти на несанкционированные уличные акции, материалы с детской порнографией, пронаркотическим и суицидальным контентом.
В суде зарегистрированы пять новых протоколов от РКН в отношении Facebook, а также по два протокола на Twitter и Telegram.

Компании обвиняются в совершении административного правонарушения, предусмотренного пунктом 2 и пунктом 4 ст. 13.41 КоАП РФ. Штраф по каждому административному делу на основе протокола РКН по пункту 2 может быть от 800 тыс. до 4 млн рублей, а по пункту 4 уже более серьезнее — от 3 до 8 млн рублей.

В итоге, суд может оштрафовать Twitter и Telegram на сумму до 12 млн рублей, а Facebook грозит административное наказание до 28 млн рублей.

Причем долг по всем текущим штрафам Facebook составляет 49 млн рублей, у Twitter штрафов по протоколам РКН на 33,4 млн рублей, а Telegram должен заплатить 26 млн рублей.

Всего суды РФ за последние 6 месяцев оштрафовали российские и зарубежные соцсети на 149 млн рублей (около $2 млн) за неудаление запрещенного контента в РФ по нескольким десяткам протоколов РКН. Пока что ни один из штрафов компании не выплатили. Некоторые начали по ним процесс обжалования, например, 23 июня это сделала Facebook и TikTok. 28 июня 2021 года Московский суд отказал TikTok в жалобе по поводу отмены штрафа на 2,6 млн рублей, как и Facebook. Тем самым суд утвердил административное наказание на компанию на основе протокола Роскомнадзора за неудаление контента с призывами к подросткам выйти на несанкционированные протестные акции в конце января.

В конце мая глава Роскомнадзора Андрей Липов рассказал, что ведомство поменяло тактику и не будет пока замедлять в России другие сервисы, такие как Google и Facebook, как сделал это с Twitter за неудаление запрещенного в РФ контента. Регулятор считает, что лучшими мерами по принуждению этих компаний к соблюдению российских законов являются многомиллионные штрафы.

Adblock test (Why?)

Технология Google повышает разрешение изображений до 16 раз без потери качества

В июле исследователи Google из команды Brain Team поделились своими достижениями в области масштабирования изображений. Результаты своих исследований они опубликовали в блоге Google AI, посвящённом исследованиям и разработкам в области машинного обучения и искусственного интеллекта.

В статье под названием «Создание высокоточных изображений с использованием диффузионных моделей» (High Fidelity Image Generation Using Diffusion Models) продемонстрирована технология масштабирования изображений на базе диффузионных моделей.

Говоря простыми словами, Image Super-Resolution — это технология «умного» увеличения изображений. Она заключается в обучении модели превращать изображение с низким разрешением в изображение с высоким разрешением (технология RAISR была описана в блоге Google AI в 2016 году).

Если быть точным, в свежей публикации описан подход, основанный на комбинации двух алгоритмов — SR3 и CDM. Он позволяет создавать изображения высокого разрешения без заметной потери качества.

SR3 - Super-Resolution via Repeated Refinements - масштабирование через повторное уточнение.

CDM - Cascaded Diffusion Models - каскадные диффузионные модели.

Super-Resolution via Repeated Refinements

SR3 принимает на вход изображение в низком разрешении и пытается построить изображение с более высоким разрешением, добавляя в него гауссовский шум и размытие на каждом повторе. Итоговое изображение по сути содержит чистый шум. Затем идёт обратный процесс - модель постепенно удаляет шум для достижения нужного результата.

Обученная на огромном массиве данных, модель SR3 показывает хорошие результаты в задачах масштабирования в 4-8 раз изображений лиц и изображений объектов живой природы: 64x64 → 256x256 (в 4 раза) и 256x256 → 1024x1024 (в 4 раза). Объединив модели в каскад, можно масштабировать изображения до 16 раз: 64x64 → 1024x1024.

Как оценить качество работы SR3? Результаты работы модели сравнивают с результатами работы других моделей. Участвующих в эксперименте людей просят выбрать изображение, которое, по их мнению, сделано на фотокамеру (так ставится вопрос).

Участники выбирают между изображением, которое создала модель, и оригинальным изображением с камеры.

Результаты масштабирования трёх алгоритмов (Bicubic, Regression, SR3): сверху — изображения с лицом (64x64 → 512x512), снизу — изображения животного (64x64 → 256x256). Оригинальное изображение в правом столбце.
Результаты масштабирования трёх алгоритмов (Bicubic, Regression, SR3): сверху — изображения с лицом (64x64 → 512x512), снизу — изображения животного (64x64 → 256x256). Оригинальное изображение в правом столбце.

Эффективность модели измеряется с помощью коэффициента путаницы (confusion rate): какой процент времени участники эксперимента выбирают результат работы модели, а не эталонное изображения (а идеальный алгоритм как раз и даёт "50-процентный коэффициент путаницы").

Результаты этого исследования показаны ниже:

Сверху: коэффициент путаницы в задаче с изображениями лиц (16x16 → 128x128). Снизу: коэффициент путаницы в гораздо более сложной задаче — с изображениями объектов живой природы (64x64 → 256x256)
Сверху: коэффициент путаницы в задаче с изображениями лиц (16x16 → 128x128). Снизу: коэффициент путаницы в гораздо более сложной задаче — с изображениями объектов живой природы (64x64 → 256x256)

Cascaded Diffusion Models

CDM обучена на огромном количестве изображений из базы ImageNet, которые представляют собой достаточно сложный набор данных. Именно по этой причине CDM построена как каскад нескольких моделей.

Каскадный подход представляет собой цепочку нескольких генеративных моделей, каждая из которых создаёт изображение с разным разрешением: одна диффузионная модель генерирует изображение с низким разрешением, которое обрабатывает SR3, постепенно повышая разрешение до максимально доступного.

GIF на 4,5 МБ

Масштабирование изображения с 32x32 до 256x256

О реальном внедрении или коммерческом применении информации пока что нет.

Изображения, созданные из изображений низкого разрешения
Изображения, созданные из изображений низкого разрешения

Дополнительные материалы:

  1. Оригинальная статья: https://ai.googleblog.com/2021/07/high-fidelity-image-generation-using.html

  2. Технология RAISR: https://ai.googleblog.com/2016/11/enhance-raisr-sharp-images-with-machine.html

  3. Метод масштабирования изображений BigGAN-deep: https://paperswithcode.com/method/biggan-deep

  4. Метод масштабирования изображений VQ-VAE-2: https://paperswithcode.com/method/vq-vae-2

Adblock test (Why?)

Прусский опыт, один из лучших в истории примеров «Революции сверху»

Мало кто из литераторов, да что там литераторов – даже нашего брата-историка, смог избежать описания драматической картины: король Пруссии Фридрих-Вильгельм III, кутаясь в плащ, наблюдает, как на на плоту, установленном на середине реки Мемель, в шатре беседуют два императора – Наполеон Бонапарт и Александр I, обсуждая новые контуры Европы. Среди вопросов, которые обсуждали два этих очень непохожих друг на друга человека, был и вопрос Пруссии: быть или не быть этому государству, быть или не быть династии Гогенцоллернов.

Мнение самого короля Пруссии никому не было ни важным, ни интересным, все должно быть решено без него.

Этот эпизод вовсе не стал концом унижений прусского короля: через несколько дней Наполеон затребует на личную встречу его жену, красавицу и умницу Луизу. Она, так и не оправившись от тяжелейшего воспаления легких, которое подхватила, спасаясь вместе с детьми от наполеоновских войн во время бегства через Куршскую косу, практически непреодолимую зимой, примчится в стан узурпатора, в слабой надежде хоть как-то повлиять на его решения относительно Пруссии.

Тильзитский мир, та самая встреча посреди реки Мемель (сейчас - Неман). Да, раньше тоже любили постановочные эффекты на встречах на высшем уровне.
Тильзитский мир, та самая встреча посреди реки Мемель (сейчас - Неман). Да, раньше тоже любили постановочные эффекты на встречах на высшем уровне.

Самого Фридриха-Вильгельма оставят ждать исхода у палатки Наполеона. Положение его было более чем двусмысленно: простояв у входа более часа, он самовольно ворвался в покои французского императора, на что Наполеон, известный женолюб, гнусно заявил: «Приди Вы на 15 минут позже, я бы вынужден был лишиться Магдебурга», намекая тем самым на то, что он готов был уже перейти к «активным действиям» в отношении королевы.

Особую пикантность этой сцене придавало то, что Наполеон был уверен, что Луиза – любовница Александра (это не так, но здесь важна не столько реальность, сколько представления Наполеона о ней).

Сложно сказать, какое из унижений Фридриха-Вильгельма стало триггером реформ в Пруссии, потому что список всех унижений был бы очень велик, но факт, что, вернувшись в Берлин, король, человек нерешительный и сомневающийся, отдал приказ немедленно приступать к реформам страны.

Как мы знаем сейчас (но об этом мог только мечтать стоящий на берегу Мемеля Фридрих-Вильгельм III), Пруссия, в итоге, была сохранена, правда, она теряла половину своих территорий и половину своего населения, кроме того, она обязана была выплатить Франции огромную контрибуцию в размере неподъемных для лишившегося самых богатых своих районов государства 154,5 млн франков, а также оплачивать постой французских войск в своих городах (к 1812 году эти расходы составят от 146 до 309 млн франков, по разным подсчетам).

Персонажи заключения Тильзитского мира на полотне художника Николя Госса. Слева направо: Наполеон, Александр I, Луиза и Фридрих-Вильгельм III. Современные историки приписывают королеве Пруссии чувство вины за то, что якобы она была чуть ли не инициатором той войны.
Персонажи заключения Тильзитского мира на полотне художника Николя Госса. Слева направо: Наполеон, Александр I, Луиза и Фридрих-Вильгельм III. Современные историки приписывают королеве Пруссии чувство вины за то, что якобы она была чуть ли не инициатором той войны.

Армия Пруссии была сокращена до 43 тысяч человек. Земли за Рейном отходили к созданному Наполеоном королевству Вестфалия, в спину Пруссии дышала восстановленная Польша в виде герцогства Варшавского. Крупнейший торговый центр Пруссии, вольный город Данциг, кажется, единственный, не сдавшийся французам (взять его удалось только после долгой осады), тоже был потерян.

Наши историки много говорят о том, что Пруссия уцелела благодаря активной позиции русского царя, выполнявшего свои обязательства перед «кузеном Фрицем-Вилли», но, конечно же, сохранение буферной зоны в виде Пруссии между двумя великими державами было важно и интересно обеим сторонам.

В те времена в Пруссии хватало образованных людей, которые понимали, как убого выглядит их страна, застрявшая в средневековье, по сравнению с великими державами, к которым относили Англию и Францию. Эти образованные люди понимали, что «английский вариант» - мирное развитие на основе промышленной революции – гораздо более привлекателен, чем «французский», революционный путь.

Пруссия и в самом деле застряла в средневековье: в стране (за исключением земель короля) господствовало крепостное право, экономической основой аграрной страны были юнкерские (так называли крупных землевладельцев) хозяйства, буквально выжимавшие соки из крепостных, в некоторых из таких хозяйств барщина доходила до 6 дней в неделю.

Все сословия Пруссии: в центре, понятно, юнкеры. Здесь же нашлось место горожанам и крестьянам.
Все сословия Пруссии: в центре, понятно, юнкеры. Здесь же нашлось место горожанам и крестьянам.

Разумеется, крестьяне не считали чем-то принципиально важным сохранение гнёта именно собственных помещиков, наоборот, французов встречали со сдержанным оптимизмом.

Земля не была предметом купли-продажи, земельный участок целиком наследовал один из юнкеров, остальные дети владельца служили в армии или на гражданской службе.

Промышленности как таковой не существовало, надлежащее ей место занимали ремесленники, объединенные цеховыми правилами, которые и за сто лет до этого выглядели архаизмом и были тормозом для развития производства.

Точно в таком же состоянии находилась и торговля. Может, даже правильнее было бы сказать, что её и вовсе не существовало, так как изнуряемое жестким контролем государства и мелкими придирками чиновников купечество было малочисленно и целиком зависело от расположения местных властей, что, конечно же, неизбежно приводило к коррупции.

Армия Пруссии, некогда очень славная и даже (правда, речь идет о событиях столетней давности) бывшая когда-то образцом для многих в Европе, основана была на принципе рекрутского набора. Офицерами были исключительно дворяне, выходцы из юнкерского сословия. Учитывая, что армия всегда была предметом особой гордости пруссаков (путь слава уже и ушла в историю), её содержание, так уж сложилось исторически, обходилось стране в копеечку: к моменту начала реформ на содержание армии расходовалось более 70% ВВП страны (заметим, тем болезненнее в обществе восприняли катастрофическое поражение, когда сначала бравые прусские офицеры считали чуть ли не своей обязанностью, бахвалясь, точить сабли о мостовую перед французским посольством, а затем, после поражений при Йене и Ауэштедте, без боя сдали все крепости и позволили врагам спокойно вступить в Берлин).

Вот как-то так вели себя бравые прусские офицеры у французского посольства до начала боевых действий. Наверное, это напугало сотрудников дипломатической миссии, но не Францию.
Вот как-то так вели себя бравые прусские офицеры у французского посольства до начала боевых действий. Наверное, это напугало сотрудников дипломатической миссии, но не Францию.

Кроме того, рекрутская система набора со всех точек зрения уступала армии Франции, в основе комплектования которой была воинская повинность – мобилизационный ресурс Пруссии сильно отставал от требований современных войн.

В области экономики, чрезвычайно зарегулированной и страдающей от тотального контроля ненужных чиновников, всё еще главенствовали уже изрядно обветшавшие идеи меркантилизма, предполагавшие, что страна должна богатеть за счет большого экспорта и низкого импорта, хотя для отсталой Пруссии, продававшей зерно и покупавшей промышленные товары, поддерживать подобный баланс было невозможно.

Несмотря на абсолютизм, страна была плохо управляема, так как основные решения принимались местными органами власти, которые, конечно же, подобострастно заглядывали в рот королю, но не обращали особого внимания на указания правительства.

О необходимости реформ в Пруссии говорили давно, причем главными «архитекторами» этих реформ выступали поступившие на службу прусскому королю выходцы из западных районов Германии – барон Фридрих Карл фом унд цум Штейн, родом из Нассау, и князь Карл Август фон Гарденбург, ганноверец.

Оба они окончили Гёттингенский университет, оба, вне всякого сомнения, входили в число лучших умов Европы, оба были страстными поклонниками Адама Смита и Тюрго. И несмотря на то, что их взгляды на экономику и социальное устройство формировались одновременно с взглядами Наполеона и во многом были им созвучны, они оба были страстными противниками корсиканца.

Неугомонный и решительный реформатор (враги называли его "бешеным") Генрих Штейн. Как видите, удостоился памяти даже в социалистической Германии — в ГДР.
Неугомонный и решительный реформатор (враги называли его "бешеным") Генрих Штейн. Как видите, удостоился памяти даже в социалистической Германии — в ГДР.

Собственно, на этом сходство между этими великими людьми заканчивается, так как холерик Штейн не подбирал слов и не признавал обходных путей, зачастую «ломая об колено» своих противников, тогда как Гарденберг проявил себя умным и чутким дипломатом. Более того, Гарденберг верил, что люди, им убежденные, принимавшие его логику как свою, и являются наиболее ценными проводниками реформ.

Гарденберг еще в 1794 году выразил свои убеждения, написав, что желал бы открыть воз­можности для карьеры талантливых людей, равномерно распределить налоговое бремя, обеспечить безопасность собственности и защиту прав личности, а также достичь со­четания свободы с религиозностью и гражданским поряд­ком. Причем все эти изменения должны были быть достиг­нуты путем преобразований, идущих исключительно сверху. Заметим, убеждения эти удивительным образом совпадают с главными принципами, которыми руководствовался тогда еще никому не известный Наполеон.

Впрочем, то, как достигались эти простые идеи во Франции, не нравилось ни Гарденбергу, ни Штейну, их больше привлекал путь Англии, где к концу XVIII века курс на промышленную революцию уже явственно обозначился. Еще до того, как Пруссия потерпела унизительное поражение в войне с Наполеоном, вокруг этих двух лидеров сложился кружок людей, почитавшихся англоманами, хотя все их «англоманство» заключалось зачастую в знании теории Адама Смита, которую тогда уже преподавали в ведущих университетах. Кроме того, реформаторы всегда ощущали поддержку королевы Луизы. И Штейн и Гарденберг написали свои меморандумы, где они высказывались о желательном направлении и характере реформ.

Знаменитая книга Адама Смита была переведена на множество языков, в том числе и на русский, и пользовалась большой популярностью у наших соотечественников (о чем писал Пушкин).
Знаменитая книга Адама Смита была переведена на множество языков, в том числе и на русский, и пользовалась большой популярностью у наших соотечественников (о чем писал Пушкин).

В Пруссии эти новые знания стали основой реформ, в России эти знания стали уделом узкого круга просвещенных. Прусские реформаторы, при всей своей очевидной симпатии к России и всему русскому не находили там никаких примеров для подражания, а русское общество, в целом, оказалось не готово к реформам.

Первый шаг выпало сделать Штейну (на использование Гарденберга на госслужбе Наполеон лично наложил вето), который был в то время министром финансов и внутренних дел. Он стал неформальным главой правительства, поставив условие, что каждый из министров имеет право обращаться к королю напрямую (раньше король не видел министров, ему докладывал исключительно премьер-министр).

Забегая вперед, скажем, что пробыл на своем посту Штейн всего лишь 14 месяцев – причиной его отставки стало перехваченное французами письмо, где Штейн, со свойственной ему прямотой, довольно грубо отзывался о Наполеоне, после чего он не только покинул правительство, но даже эмигрировал.

Первая встреча Штейна с королем состоялась 30 сентября 1807 года, 5 октября Штейн был утвержден министром, а уже 9 октября появился королевский эдикт, отменяющий крепостное право. Медлить вообще было не в его характере, и он первым делом устранил главное препятствие на пути трансформации Пруссии. Конечно, решить вот так вот, мгновенно, одним росчерком пера все проблемы, связанные с крепостничеством, не вышло, в конце концов, юнкеры, основа государства, представляли собой мощную политическую силу (отсутствие «энтузиазма» в их среде выразил один из лидеров юнкерства, барон фон Рекк: «Лучше три Ауеэштедта, чем один октябрьский эдикт»), и законы, то откатываясь назад, то, напротив, совершенствуясь и развиваясь, будут меняться до начала второй половины века. Однако главное было сделано: появилось огромное количество лично свободных граждан страны, которые вольны были распоряжаться своей судьбой и своей жизнью, (например, получать образование или жениться без приказов), существующих в едином правовом пространстве, устранено рабство.

Конечно, реформы оказались болезненны и для части крестьянства. За годы их проведения около 100 тысяч семей (население Пруссии к моменту начала реформ составляло ок. 4,5 млн человек, а к моменту их окончания, к 1850 году, – чуть менее 17 млн) потеряли всё имущество, но именно они и составили основу формирующегося рабочего класса страны, костяк индустриализации.

Карл Август фон Гарденберг, человек, проведший сложнейшие реформы в Пруссии
Карл Август фон Гарденберг, человек, проведший сложнейшие реформы в Пруссии

Штейн успел заложить основы управления страной, которая теперь управлялась не наместниками короля на местах, а кабинетом министров (позже Гарденберг сделает то, что не вышло у Штейна, – создаст государственный совет, куда войдут нынешние и бывшие министры, оппозиционные юнкеры – Гарденберг всегда считал, что совместно принятые решения скрепляют их и придают большую легитимность), что постепенно, но неотвратимо, убирало абсолютизм как форму правления с политической сцены страны. Руководили регионами теперь не князья, подчинявшиеся королю, а обер-президенты, назначаемые советом министров.

Также удалось навести порядок в области единства законов и обеспечения права в городах, где исторически сложившиеся городские советы на фоне абсолютизма влачили жалкое существование, а исторически сложившиеся разнородные законы могли довольно сильно отличаться в разных городах.Реформаторы усилили роль городских советов в управлении своими территориями, но ввели единую полицию, единые законы и суд.

Одной из важнейших реформ стала реформа армии. Здесь у Штейна и Гарденберга было немало сторонников из числа высших военных чинов, именно им и доверено было переустройство военного ведомства. Возглавил реформы Герхард Шарнхорст (редкий тогда случай выходца из низов общества) и его соратники – в частности, фон Гнейзенау, сменивший Шарнхорста на посту военного министра после его смерти.

Герхард Шарнхорст, генерал, выходец из низов общества (редчайший случай для Пруссии того времени), его отец - вахмистр в отставке, ставший арендатором у юнкера. Создал одну из самых боеспособных армий своего времени. Погиб от ран, полученных в бою, в 1813 году.
Герхард Шарнхорст, генерал, выходец из низов общества (редчайший случай для Пруссии того времени), его отец - вахмистр в отставке, ставший арендатором у юнкера. Создал одну из самых боеспособных армий своего времени. Погиб от ран, полученных в бою, в 1813 году.

В армии отменены физические наказания; любому, независимо от происхождения, доступно продвижение по карьерной лестнице; введены военные училища и даже Военная академия для высших чинов. Шарнхорст и Гнейзенау реформируют незавидную прусскую артиллерию, в армии появляются даже снайперские группы (кажется, впервые в истории).

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

В 1813 году появляется Ландвер – военная структура, в которую входят все годные к военной службе мужчины, – там они получают минимальные боевые навыки.

Позже будет сформирован даже ландштурм – войска, состоящие из непригодных к воинской службе, в том числе стариков, которых планировалось обучить ведению партизанской войны в случае, если враг оккупирует их территорию. Впрочем, это экзотическое подразделение так никогда и не использовалось.

И Гарденберга, и Штейна очень заботили реформы в экономике, которые, по сравнению с вышеописанными, были не так заметны, но на жизнь общества и на быстрые перемены в укладе страны оказавшие огромное влияние. Штейн ушел со своего поста в 1808-м, после чего несколько раз менялись лидеры реформистского направления, пока, наконец, в 1810 году за дело не взялся Гарденберг.

Он провел закон об эмансипации (равенстве) евреев, новый закон о торговле, позволяющий заниматься этим делом любому свободному гражданину страны, ввел профессиональные налоги (для торговли и промышленности). И наконец, только в 1818 году ему удалось сделать то, о чем он мечтал так долго: появился закон о единой таможне и об уничтожении пошлин при торговле внутри Пруссии. Позже эта идея будет развернута как таможенный союз, к которому вольны будут присоединиться любые немецкие земли и территории (и они этим, конечно же, воспользуются), – так была заложена основа экономического единства будущей Германии и роли Пруссии как «объединителя нации».

Деревня в Пруссии, XIX век. Да, Пруссия станет страной крупных землевладений и лидером по эффективности аграрного сектора. Правда, в конце XIX-начале ХХ веков даже этому высокоэффективному хозяйству нечего будет противопоставить потоку дешевой пшеницы, масла и мяса из Канады, России, США и Аргентины, где экстенсивное землепользование дает низкую себестоимость. И Германия еще с большим усердием возьмется за индустриализацию.
Деревня в Пруссии, XIX век. Да, Пруссия станет страной крупных землевладений и лидером по эффективности аграрного сектора. Правда, в конце XIX-начале ХХ веков даже этому высокоэффективному хозяйству нечего будет противопоставить потоку дешевой пшеницы, масла и мяса из Канады, России, США и Аргентины, где экстенсивное землепользование дает низкую себестоимость. И Германия еще с большим усердием возьмется за индустриализацию.

Профессора из Кёнигсберга Теодор фон Шён и Кристиан Якоб Краус, поклонники всё того же Адама Смита, разработали модель промышленного развития Германии, указ об отмене цеховых привилегий и обязательности участия в гильдиях. Их главной задачей было создание конкурентной среды, а это предполагало отсутствие любых ограничений (разного рода разрешений, просьб открыть дело или завести торговые сношения с другими странами, никаких патентов и прочего, чем грешила предшествующая политика), что, в общем, было успешно реализовано.

Забегая немного вперед, заметим, что принципы свободной торговли, исповедуемые реформаторами, принесут в какой-то момент множество проблем: после окончания наполеоновских войн страна окажется наводнена английскими промышленными товарами, лучшими на тот момент в мире, и будет раздаваться множество голосов в пользу протекционизма, правительства будут метаться в сторону то смягчения, то ужесточения норм импорта, но в конце концов заложенный на свободную торговлю курс окажется верным: прусская промышленность, вынужденная конкурировать с самой лучшей продукцией в мире, сама выйдет на высочайший уровень качества и вскоре станет успешно конкурировать с британской и американской по всему миру.

Однако, кажется, самой разрушительной для абсолютизма и самой впечатляющей по своей мощи оказалась реформа образования.

Адольф фон Менцель, Железопрокатный завод. Реформы дали старт процессу, который позже назовут "промышленной революцией в Германии", и началом его принято считать 1815 год (конечно, условно). Вскоре Германия войдет в число самых развитых стран мира и даже будет задавать тон в инновациях и изобретениях.
Адольф фон Менцель, Железопрокатный завод. Реформы дали старт процессу, который позже назовут "промышленной революцией в Германии", и началом его принято считать 1815 год (конечно, условно). Вскоре Германия войдет в число самых развитых стран мира и даже будет задавать тон в инновациях и изобретениях.

Ведущую роль в этом сыграл Вильгельм Гумбольдт, один из самых известных прусских ученых. С 1808 года он возглавил департамент по делам образования и религии в правительстве Штейна и вскоре написал трактат о системности образования, где выделял три ступени: народные школы, гимназии и университет, обосновав задачи каждой из них.

Все остальные виды школ – религиозные, частные, местные и прочие (их насчитывалось огромное количество) – подлежали преобразованию. Программы подобных школ были разнообразны, но ненаучны.

Королева Луиза была большой поклонницей Песталоцци, швейцарца, выдвинувшего гуманистические идеи развивающего обучения (Ушинский считал эти принципы как одно из величайших открытий), и идеи Песталоцци и Фихте были заложены в начальную школу, школу первой ступени (у Гумбольдта – народная школа), которая стала бесплатной и обязательной для всех.

Кроме этого Гумбольдт ввел принцип «абитур», выпускных и вступительных экзаменов, которые были введены по всей Пруссии уже в 1812 году.

Народные школы требовали большого количества преподавателей, и в стране были организованы учительские семинарии.

Вильгельм Гумбольдт, великий ученый и великий реформатор, создавший в Пруссии самую лучшую в мире на тот момент (неустаревшую и ныне) систему образования, благодаря которой миру открылось столько гениев.
Вильгельм Гумбольдт, великий ученый и великий реформатор, создавший в Пруссии самую лучшую в мире на тот момент (неустаревшую и ныне) систему образования, благодаря которой миру открылось столько гениев.

Учебные программы всех уровней были пересмотрены, была полностью «вычищена» зубрежка, все курсы были организованы на принципах научного знания.

Возросли и требования к государственным служащим: теперь для занятия какой-либо должности нельзя было просто быть наследником какого-то знатного рода – путь к карьере открывали знания, университетский диплом и хорошие оценки на экзаменах, то есть пробиться в элиту общества стало возможным даже для выходцев из крестьянского сословия, и, если мы посмотрим биографии немецких ученых, то нельзя будет не обратить внимание, что большинство из них – выходцы из очень простых семей – крестьян, мелких торговцев, рабочих, ремесленников.

Всего через пару десятилетий немецкая наука станет ведущей в мире, а немецкая промышленность, благодаря необыкновенно высокому образовательному уровню, станет доминирующей, и именно образовательная реформа сделает все прочие перемены в обществе необратимыми.

Впрочем… угроза реформам пришла совершенно неожиданно оттуда, откуда её меньше всего ждали.

И снова в шаткое положение усилия реформаторов поставила война с Наполеоном.

Наши школьные учебники рисуют картину войны с Наполеоном как ровную линию: как принялся бежать от Москвы, так и добежал до Парижа без остановок (правда, учебники вскользь упоминают непонятное – какую-то «Битву народов» под Лейпцигом – ну, может, какой-то малозначительный эпизод).

Иоганн Петер Крафт. Фельдмаршал князь Шварценберг (австриец, главнокомандующий войсками коалиции) сообщает венценосцам о победе в битве при Лейпциге.
Иоганн Петер Крафт. Фельдмаршал князь Шварценберг (австриец, главнокомандующий войсками коалиции) сообщает венценосцам о победе в битве при Лейпциге.

На самом деле, конечно, все было не так, Франция, благодаря современной постановке военного дела, обладала огромным мобилизационным ресурсом, и Наполеон довольно быстро увеличил свою армию с 30 тысяч человек до 130 тысяч, а позже и до 400 тысяч. В это время в немецкоязычных землях уже вовсю полыхает народная война против Наполеона, которую ставший очень осторожным Фридрих-Вильгельм III признает (только заключив предварительный союз с Россией и Австрией) «своей» войной в 1813 году, выпустив «обращение к народу».

Наполеон еще несколько раз одержит крупные победы над союзниками, и даже в какой-то момент будет казаться, что перелом наступил, но та самая «Битва народов» у стен Лейпцига, где четыре дня его 190-тысячная армия сражалась с 300-тысячной армией союзников (вплоть до Первой мировой это сражение будет оставаться крупнейшим в истории Европы), и где в итоге войска шестой антинаполеоновской коалиции одержат победу, внесет окончательную ясность в исход компании. И это тоже еще не станет концом войны, неугомонный корсиканец снова и снова будет набирать новые армии и одерживать многие локальные победы, тем не менее под давлением численно превосходящего врага он будет отступать и отступать, пока, наконец, армии коалиции не возьмут Париж и не арестуют Наполеона.

Заметим, что прусские войска под руководством фельдмаршала Блюхера проявят себя в этой войне во всей красе, а позже именно Блюхер принесет союзникам еще и победу в сражении под Ватерлоо.

Фельдмаршал Гебхард Блюхер, человек боя, человек-война, "черный генерал" и "генерал вперед!", военачальник, восстановивший славу прусской армии.
Фельдмаршал Гебхард Блюхер, человек боя, человек-война, "черный генерал" и "генерал вперед!", военачальник, восстановивший славу прусской армии.

Но вот что касается Фридриха-Вильгельма III, то, в его представлении, победа над Наполеоном, кажется, автоматически означала остановку реформ. Мол, ау, ребята, перестаньте уже, и так все хорошо вышло, цель-то достигнута…

К счастью (для Пруссии), Фриц-Вилли, что называется, не на тех нарвался. Блестящий дипломат Гарденберг обыграет своего короля как младенца: покладисто соглашаясь с ним, он будет последовательно продолжать делать то, что делал, иногда напоминая королю, что, наверное, надо остановиться, но только надо сделать бы еще вот это и это в память о королеве Луизе (интроверт Фридрих-Вильгельм, кажется, ценил и любил единственного человека на планете — свою жену, умершую в 1810 году, которая и в самом деле была горячей сторонницей перемен).

Реформы удивительно преобразят Пруссию. Уже в первой половине XIX века она станет одной из самых развитых европейских держав, с рекордным для Европы приростом населения, с ростом одного лишь аграрного производства на 40% (при том же количестве пахотных земель), страной со стремительно развивающейся промышленностью, самой передовой наукой во всех отраслях человеческих знаний, и даже страной мыслителей и поэтов.

Что касается самих реформаторов, то Гарденберг, обаятельный и дипломатичный вершитель судеб Пруссии, а позже, в определенной мере, и всей Европы, умрет в 1822 году. Штейн выскажется о его смерти в том духе, что немцы должны быть, наконец-то, счастливы.

Да, великие реформаторы недолюбливали друг друга, хотя холерик Штейн о своей нелюбви (если не сказать - ненависти) к Гарденбергу говорил постоянно, а Гарденберг выражал свои чувства крайне редко и сдержанно. Практически не существует изображений, где они были бы вместе, но вот более позднему рисовальщику пришла в голову такая картинка... Тем не менее прусские реформы названы двумя именами, и - заслуженно.
Да, великие реформаторы недолюбливали друг друга, хотя холерик Штейн о своей нелюбви (если не сказать - ненависти) к Гарденбергу говорил постоянно, а Гарденберг выражал свои чувства крайне редко и сдержанно. Практически не существует изображений, где они были бы вместе, но вот более позднему рисовальщику пришла в голову такая картинка... Тем не менее прусские реформы названы двумя именами, и - заслуженно.

Да, великие реформаторы не любили друг друга, и при всем сходстве взглядов людьми были очень разными. Штейн, типичный холерик, яростный и бескомпромиссный, проводя демократические по своей сути реформы, одновременно напоказ гордился тем, что никто в его роду не подчинялся никому, кроме императоров Священной Римской империи. Гарденберг, настоящий гуманист и этакий светский лев, обаятельный и обходительный, умел влюбить в себя собеседника и заставить говорить его словами (кстати, наблюдательный Наполеон после встречи с королевой Луизой назвал её «попугаем Гарденберга» - видимо, она невольно пользовалась какими-то оборотами из лексикона своего министра).

Поздние историки обратят внимание на то, что реформы, при всей своей жесткости и ниспровержении того, что еще совсем недавно казалось устоями и скрепами общества, обошлись без восстаний и заговоров, и припишут это удивительному умению Гарденберга объясняться с самыми разными людьми, слышать и понимать их точку зрения и тактично и умно объяснять свою собственную позицию.

Говорят, что Гарденберг пользовался большим успехом у женщин, и Штейн не мог простить ему нежного обращения с его сестрой…

Сам Штейн умрет в 1831 году. Значительная часть его жизни будет посвящена борьбе с Наполеоном: после своей отставки он в 1812 году создаст русско-германский добровольческий легион, который будет сражаться в Отечественной войне, а позже – участвовать в освобождении немецких земель, затем возглавит штаб повстанческого движения в Германии. На Венском конгрессе он будет представлять Россию (и станет кавалером высших её орденов). После завершения своей дипломатической миссии создаст и возглавит Monumenta Germaniae Historica, общество по изучению средневековой истории Германии.

Реформы в Пруссии сделали эту страну бесспорным лидером в глазах немцев, тем образцом, к которому стоит стремиться, поэтому нет ничего удивительно в том, что именно Пруссия станет центром притяжения для немцев и объединителем Германии
Реформы в Пруссии сделали эту страну бесспорным лидером в глазах немцев, тем образцом, к которому стоит стремиться, поэтому нет ничего удивительно в том, что именно Пруссия станет центром притяжения для немцев и объединителем Германии

При всей своей несхожести оба великих реформатора провели в итоге одну из самых замечательных и эффективных «революций сверху» в истории, правда, и с историческим моментом им повезло: если иметь в виду не унижения, которым подвергся император, а то, насколько страна оказалась готова к реформам и как умно и своевременно они смогли донести до общества необходимость преобразований.

Критики Штейна и Гарденберга насмешливо говорили, что то, на что у Пруссии ушло несколько лет, во Франции произошло за считанные дни, но, возможно, критиканы не учли цену вопроса, выраженную в человеческих жизнях и судьбах.

Автор: Александр Иванов

Adblock test (Why?)