Есть только два типа языков: те, на которые люди жалуются, и те, которые никто не использует - Бьярн Страуструп

Мне нравится эта цитата. он объясняет как JavaScript, так и Haskell. И в этом смысле препроцессор - отличный язык, потому что люди его много используют. Его никогда не рассматривали отдельно от C и C ++, но если бы это было так, он был бы языком номер один на TIOBE. Препроцессор чрезвычайно полезен и широко распространен. По правде говоря, было бы * действительно * сложно написать какое-либо серьезное и портативное приложение на C ++ без участия препроцессора в какой-то момент.

- препроцессор отстой

- Я точно знаю? Это самое худшее. Эй, можешь слить мой коммит? Я добавил кучу полезных макросов.

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

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

Тем не мение.

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

Это невозможно. Никогда не было и, наверное, никогда не будет.

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

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

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

Другая не менее важная проблема заключается в том, что компилятор не может знать, какая комбинация операторов #ifdef и #defines должна формировать допустимую программу.

В качестве примера Qt предлагает набор defines, который можно установить для включения или отключения определенных функций Qt во время компиляции. Допустим, вам не нужен виджет календаря, вы можете определить #QT_NO_CALENDAR_WIDGET, и это уменьшит размер двоичного файла. Не работает. Я подозреваю, что это никогда не сработало. Смотрите, в какой-то момент у Qt было около 100 таких параметров конфигурации времени компиляции. Учитывая, что количество возможных конфигураций сборки растет экспоненциально с количеством переменных. когда у вас может быть 2¹⁰⁰ вариант вашей программы, автоматизация оказывается сложной даже в масштабе большой паутины, глубокого облака, шестнадцатеричной системы.

Непроверенный код - это неработающий код.

Вы, наверное, знаете эту известную пословицу. Так что насчет даже не скомпилированного кода?

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

Препроцессор считается вредным, что с этим делать?

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

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

1. Strongly Prefer константы выше #define

Это достаточно просто, но я все еще вижу множество констант, определенных с помощью макросов. Всегда используйте static const или constexpr, а не define. Если ваш процесс сборки включает установку набора переменных, таких как номер версии или хэш git, рассмотрите возможность создания исходного файла, а не использования определений в качестве параметров сборки.

2. Функция всегда лучше макроса

Приведенный выше фрагмент взят из Win32 API. Даже для «простого» и короткого лайнера всегда следует отдавать предпочтение функции.

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



3. Избавьтесь от проблем переносимости.

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

4. Ограничьте количество вариантов вашего программного обеспечения.

Должна ли эта зависимость быть необязательной?

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

Должен ли этот код действительно выполняться только в режиме выпуска?

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

Следует ли действительно отключать эту функцию?

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

5. Отдавайте предпочтение прагме перед включением.

В настоящее время очень мало экзотических компиляторов C ++, не поддерживающих #pragma once. Использование #pragma once менее подвержено ошибкам, проще и быстрее. Поцелуй охранников на прощание.

6. Предпочитайте больше кода большему количеству макросов.

Хотя его нужно адаптировать к каждой ситуации, в большинстве случаев не стоит заменять несколько токенов C ++ макросом. Играйте в рамках правил языка, не пытайтесь быть излишне умным и терпеть небольшое повторение, он, вероятно, будет таким же читабельным, более удобным в обслуживании, и ваша IDE будет вам благодарна.

7. Очистите свои макросы.

Макросы должны быть не определены с помощью #undef как можно скорее. никогда не помещайте недокументированный макрос в файл заголовка.

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

Если вы используете сторонний фреймворк, такой как Qt, который имеет как короткие, так и длинные имена макросов (signal и QT_SIGNAL), обязательно отключите первое, особенно если они могут просочиться как часть вашего API. Не называйте сами такие короткие имена. Имя макроса должно отличаться от остального кода и не конфликтовать с boost::signal или std::min.

8. Избегайте размещения блока ifdef в середине оператора C ++.

foo( 42,
#if 0
  "42",
#endif
 42.0
);

В приведенном выше коде есть несколько проблем. Его трудно читать, сложно поддерживать и вызовет проблемы с такими инструментами, как clang-format. И это тоже сломано.

Вместо этого напишите два разных утверждения:

#if 0
foo(42, "42", 42.0);
#else
foo(42, 42.0);
#endif

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

9. Предпочитайте static_assert #error

Просто используйте static_assert(false), чтобы не выполнить сборку.

Препроцессор будущего прошлого

Хотя предыдущий совет применим к любой версии C ++, появляется все больше способов помочь вам сократить ежедневное потребление макросов, если у вас есть доступ к достаточно свежему компилятору.

1. Отдавать предпочтение модулям перед включением

Хотя модули должны улучшать время компиляции, они также создают барьер, из которого не могут просочиться макросы. В начале 2018 года не было готового к производству компилятора с этой функцией, но GCC, MSVC и clang реализовали его или находятся в процессе.

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

2. По возможности используйте if constexpr вместо #ifdef.

Когда отключенный путь кода правильно сформирован (не относится к неизвестным символам), if constexpr является лучшей альтернативой #ifdef, поскольку отключенный путь кода по-прежнему будет частью AST и проверяться компилятором и вашими инструментами, включая ваш статический анализатор. и рефакторинг программ.

3. Даже в мире постмодерна вам, возможно, придется прибегнуть к #ifdef, поэтому подумайте об использовании постмодернистского.

Хотя они и не помогают решить проблему, набор макросов стандартизируется для определения набора стандартных возможностей, предлагаемых вашим компилятором. Используйте их, если нужно. Мой совет - придерживаться функций, предлагаемых всеми компиляторами, на которые вы нацелены. Выберите базовую линию с ним. Учтите, что может быть проще перенести современный компилятор в вашу целевую систему, чем писать приложение на C ++ 98.

4. Используйте std :: source_location вместо __LINE__ и __FILE__.

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

Долгий путь к приложениям без макросов

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

1. Замените -D переменными, определенными компилятором.

Один из наиболее частых вариантов использования define - это запрос среды сборки. Отладка / выпуск, целевая архитектура, операционная система, оптимизация…

Мы можем представить себе набор констант, представленных через std::compiler, чтобы раскрыть некоторые из этих переменных среды сборки.

if constexpr(std::compiler.is_debug_build()) {  }

Точно так же мы можем представить себе, что в исходном коде объявлены какие-то extern compiler constexpr переменные, но определены или перезаписаны компилятором. Это будет иметь реальное преимущество перед constexpr x = SOME_DEFINE; только в том случае, если есть способ ограничить значения, которые могут содержать эти переменные.

Может что-то в этом роде

enum class OS {
    Linux,
    Windows,
    MacOsX
};
[[compilation_variable(OS::Linux, OS::Windows, OS::MacOsX)]]  extern constexpr int os;

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

2. Дополнительные атрибуты

Атрибуты C ++ великолепны, и их должно быть больше. [[visibility]] было бы отличным местом для начала. он может принимать переменную constexpr в качестве аргумента для переключения с импорта на экспорт.

3. Взять страницу из книги Руста.

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

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

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

Рассмотрим следующий код:

[[static_if(std::compiler.arch() == "arm")]]
void f() {}

void foo() {
    if constexpr(std::compiler.arch() == "arm") {
        f();
    }
}

У него удивительное свойство: он хорошо сформирован. Поскольку компилятор знает, что f является допустимой сущностью и что это имя функции, он может однозначно проанализировать тело отклоненного оператора if constexpr.

Вы можете применить один и тот же синтаксис к любому виду объявления C ++, и компилятор сможет понять его.

[[static_if(std::compiler.arch() == "arm")]]
int x = /*...*/

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

[[static_if(std::compiler.is_debugbuild())]]
class X {
};

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

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

Однако это непросто, как кажется. Что, если тело отброшенных сущностей содержит синтаксис, о котором текущий компилятор не знает? Может быть, расширение поставщика или какая-то более новая функция C ++? Я думаю, что логично, чтобы синтаксический анализ происходил на основе максимальных усилий, и когда происходит сбой синтаксического анализа, компилятор может пропустить текущий оператор и предупредить о частях источника, которые он не понимает. «Мне не удалось переименовать Foo между строками 110 и 130» - это намного лучше, чем «Я переименовал несколько экземпляров Foo. Может быть, не все, удачи в просмотре всего проекта вручную, на самом деле не беспокойтесь о компиляторе, просто используйте grep ».

4. constexpr все вещи.

Может нам понадобится constexpr std::chrono::system_clock::now() для замены __TIME__

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

5. Сгенерируйте код и символы с помощью отражения.

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

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

int declname("foo", 42) = 0; создает переменную foo42. Учитывая, что конкатенация строк для формирования новых идентификаторов является одним из наиболее частых случаев использования макросов, это действительно очень интересно. Надеюсь, у компилятора будет достаточно информации о символах, созданных (или упомянутых) таким образом, чтобы правильно их проиндексировать.

Печально известный X macro также должен уйти в прошлое в ближайшие годы.

6. Чтобы избавиться от макросов, нам нужны макросы нового типа.

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

Это тема, о которой я думал в прошлом году



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

Как это будет работать?

Хорошо, что здесь происходит?

Сначала мы создаем constexpr block с constexpr { }. Это часть предложения мета-класса. Блок constexpr - это составной оператор, в котором все переменные constexpr и не имеют побочных эффектов. Единственная цель этого блока - создать фрагменты внедрения и изменить свойства объекта, в котором объявлен блок, во время компиляции. (Метаклассы - это синтаксический сахар поверх блоков constexpr, и я бы сказал, что на самом деле нам не нужны метаклассы.)

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

Они используют оператор инъекции ->. -> можно использовать для описания всех операций, связанных с внедрением кода, без противоречия с его текущим использованием. В вашем случае, поскольку журнал является синтаксическим макросом, который является формой внедрения кода, мы определяем макрос с помощью log->(){....}.

Тело синтаксического макроса само по себе является блоком constexpr, который может содержать любое выражение C ++, которое может быть вычислено в контексте constexpr.

Он может содержать 0, один или несколько операторов внедрения, обозначенных -> {}. Оператор внедрения создает фрагмент кода и немедленно внедряет его в точку вызова, которая, в случае синтаксического макроса, является местом, откуда макрос раскрывается.

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

Хотя у него нет типа, его природа определяется компилятором.

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

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

С точки зрения реализации, в приведенном выше примере std::meta::expression<char*> - это концепция, соответствующая любому отражению в выражении, тип которого - char*.

Последнее волшебство при оценке макроса заключается в том, что выражения неявно преобразуются в свое отражение перед раскрытием.

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

Наконец, когда мы вводим print(->c, ->(args)...), обратите внимание на токены ->. Это преобразует отражение обратно в исходное выражение, которое затем можно оценить.

С места вызова log->("Hello %", "World"); выглядит как обычный вызов функции void, за исключением того, что -> указывает на наличие раскрытия макроса.

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

std::reflexpr->(x) может расшириться до __std_reflexpr_intrasics(x) до вычисления x.

Полностью ли S-Macro заменяет макросы препроцессора?

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

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

Они действительно решают проблему ленивого вычисления и условного исполнения. Например, вы не можете реализовать с ними foreach, поскольку for(;;) не является полным утверждением (for(;;); и for(;;){} являются, но они не очень полезны).

По поиску имени возникает много вопросов. Должен ли макрос «видеть» контекст, в котором он развернут? Должен ли аргумент знать о внутренней стороне макроса? это контекст объявления.

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

Это реальная жизнь ?

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

Он немного напоминает ржавые макросы - за исключением того, что не допускает произвольных операторов в качестве аргументов - хотя (я надеюсь) ощущается как часть C ++, а не как другой язык с отдельной грамматикой.

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

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

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

#undef