Создать путь для директивы #include с помощью макроса

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

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

#include TARGET_PATH_OF(header.h)

Что расширится до чего-то вроде этого:

#include "corefoundation/header.h"

когда источник настроен (в данном случае) для OSX

Пока все попытки провалились. Я надеюсь, что кто-то там сделал это раньше?

пример того, что не работает:

#include <iostream>
#include <boost/preprocessor.hpp>

#define Dir directory/
#define File filename.h

#define MakePath(f) BOOST_PP_STRINGIZE(BOOST_PP_CAT(Dir,f))
#define MyPath MakePath(File)

using namespace std;

int main() {
    // this is a test - yes I know I could just concatenate strings here
    // but that is not the case for #include
    cout << MyPath << endl;
}

ошибки:

./enableif.cpp:31:13: error: pasting formed '/filename', an invalid preprocessing token
    cout << MyPath << endl;
            ^
./enableif.cpp:26:16: note: expanded from macro 'MyPath'
#define MyPath MakePath(File)
               ^
./enableif.cpp:25:40: note: expanded from macro 'MakePath'
#define MakePath(f) BOOST_PP_STRINGIZE(BOOST_PP_CAT(Dir,f))
                                       ^
/usr/local/include/boost/preprocessor/cat.hpp:22:32: note: expanded from macro 'BOOST_PP_CAT'
#    define BOOST_PP_CAT(a, b) BOOST_PP_CAT_I(a, b)
                               ^
/usr/local/include/boost/preprocessor/cat.hpp:29:36: note: expanded from macro 'BOOST_PP_CAT_I'
#    define BOOST_PP_CAT_I(a, b) a ## b
                                   ^
1 error generated.

person Richard Hodges    schedule 18.08.2015    source источник


Ответы (4)


Я склонен согласиться с комментарием в ответе utnapistim о том, что вы не должны этого делать, даже если можете. Но на самом деле вы можете это сделать с помощью стандартных компиляторов C. [Примечание 1]

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

На самом деле вам не нужно объединять токены, чтобы преобразовать их в строку с помощью оператора #, поскольку этот оператор преобразует в строку весь аргумент макроса, а аргумент может состоять из нескольких токенов. Однако stringify учитывает пробелы [Примечание 2], поэтому STRINGIFY(Dir File) не будет работать; это приведет к "directory/ filename.h", а лишний пробел в имени файла приведет к сбою #include. Итак, вам нужно объединить Dir и File без пробелов.

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

#define IDENT(x) x
#define XSTR(x) #x
#define STR(x) XSTR(x)
#define PATH(x,y) STR(IDENT(x)IDENT(y))
 
#define Dir sys/
#define File socket.h

#include PATH(Dir,File)

Предупреждение: (Спасибо @jed за решение этой проблемы.) Если объединяемые строки содержат идентификаторы, которые определены в другом месте как макросы, то здесь произойдет непредвиденная подстановка макросов. Следует соблюдать осторожность, чтобы избежать этого сценария, особенно если Dir и/или File не контролируются (например, путем определения в качестве параметра командной строки при вызове компилятора).

Вы также должны знать, что некоторые реализации могут определять слова, которые могут отображаться в виде токенов в пути к файлу. Например, GCC может определять макросы с такими именами, как unix и linux, если только он не вызывается с явным стандартом C (что не является значением по умолчанию). Это может быть вызвано такими путями, как platform/linux/my-header.h или даже linux-specific/my-header.h.

Чтобы избежать этих проблем, я бы рекомендовал, если вы используете этот хак:

  • вы используете настройку компилятора, совместимую со стандартами C (или C11), и

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

Кроме того, вам не понадобилось бы усложнение макроса IDENT, если бы вы могли записать конкатенацию без пробелов. Например:

#define XSTR(x) #x
#define STR(x) XSTR(x)

#define Dir sys
#define File socket.h

#include STR(Dir/File)

Примечания

  1. Я попробовал это с clang, gcc и icc, которые доступны на godbolt. Я не знаю, работает ли он с Visual Studio.

  2. Точнее, он частично учитывает пробелы: пробелы преобразуются в один символ пробела.

person rici    schedule 18.08.2015
comment
Потрясающие. спасибо, что уделили этому время. Я пока использую clang и gcc. windows_mobile будет последним препятствием и, вероятно, самым высоким во многих областях, поскольку мы используем C++14. - person Richard Hodges; 18.08.2015
comment
Спасибо за это .. Моя ситуация явно заключалась в том, что я не мог использовать значения пути включения из-за коллизий. Пока я не наведу порядок, это сделает мою жизнь немного более управляемой. - person James; 02.09.2015
comment
Обратите внимание на побочные эффекты расширения, выполняемого внутри путей. Например, #include PATH(linux/,userfaultfd.h) не работает с диалектами gnu C/C++ (по умолчанию для gcc и clang), потому что они определяют linux=1, что приводит к попытке включить "1/userfaultfd.h". Аналогичные проблемы возникают при построении пути типа foo-unix/bar.h (поскольку unix=1 также определен). Вы должны быть уверены, что ваша система никогда не будет обрабатывать такие пути, прежде чем применять этот метод. - person Jed; 23.06.2019
comment
@rici Вы можете избегать этих конкретных макросов для каждого компилятора, но вам могут не понадобиться такие требования в заголовке, который вы распространяете среди пользователей. Я только что столкнулся с этой техникой, используемой для обработки абсолютного пути, которая потерпела неудачу из-за имени родительского каталога. - person Jed; 23.06.2019
comment
Вы можете использовать -std=c++11, чтобы избежать макросов linux и unix, но многие проекты определяют другие макросы, которые могут появиться в пути (возможно, в родительском каталоге). - person Jed; 23.06.2019
comment
@jed: Хорошо, я добавил предупреждение. Спасибо за предложение. - person rici; 23.06.2019
comment
Я могу подтвердить, что это работает с Visual Studio 2019. (И я заметил кого-то в другом ответе с тем же решением для VS2013.) - person celticminstrel; 19.11.2019
comment
@rici Можно ли заставить это решение работать таким образом, чтобы макрос STR мог принимать строковые литералы, например: #include STR("some/path/", "myfile.h")? - person IvanR; 14.07.2020
comment
@IvanR: Нет. В препроцессоре нет механизма для удаления кавычек из токена, а конкатенация строковых литералов происходит после предварительной обработки, поэтому ее нельзя использовать в директиве #include. Поэтому вам нужно выяснить, как это сделать без кавычек. Для путей к файлам это обычно не проблема. Как правило, включаемые пути лучше настраивать с помощью средств вашей среды сборки, которая должна быть лучше адаптирована к поставленной задаче. - person rici; 14.07.2020
comment
Я считаю, что второй экземпляр IDENT(), IDENT(y) в тексте замены PATH() излишен: цель IDENT() состоит в том, чтобы разграничить свой аргумент от следующего токена предварительной обработки без введения пробела, а y является конечным токеном. # define PATH(x,y) STR(IDENT (x)y) В любом случае большое спасибо за это. - person user1254127; 06.06.2021
comment
@user1254127: user1254127: это, конечно, не нужно, но это ничему не повредит, и мне было легче читать. Но твой способ так же хорош. - person rici; 06.06.2021

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

Вы должны быть не в состоянии (а если вы можете это сделать, вы, вероятно, не должны этого делать).

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

Каноническое решение:

Используйте IF в вашем Makefile или CMakeLists.txt, используйте настраиваемые страницы свойств в зависимости от конфигурации сборки в Visual Studio (или просто задайте определенные параметры сборки в среде ОС для вашего пользователя).

Затем напишите директиву include следующим образом:

#include <filename.h> // no path here

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

person utnapistim    schedule 18.08.2015
comment
Вы можете квалифицировать, почему это не должно быть возможно? Добавление директивы -I для исходных файлов в этой части программы является вариантом, но это (на мой взгляд) запутает код, потому что больше не будет очевидно, что требуемый заголовок находится в подкаталоге, который был настроен в CMakeLists. текстовый файл - person Richard Hodges; 18.08.2015
comment
Это не должно быть возможно, потому что исходный код не место для настройки путей поиска включения для среды сборки (если бы мне пришлось писать компилятор или спецификацию компилятора, я бы не добавлял замену токена макроса в параметры директивы включения, но я не не знаю есть ли). Если вы хотите сделать очевидным, что включаемый файл принадлежит определенному каталогу, добавьте путь включения до подкаталога в системе сборки, а также подкаталог и имя файла в исходном коде. - person utnapistim; 18.08.2015
comment
Такое понятие кажется фактически эквивалентным размещению всех заголовочных файлов проекта в одном каталоге, за исключением того, что конфликты имен не приведут к перезаписи файлов, а вместо этого могут привести к произвольному выбору системой среди файлов с одинаковыми именами. Возможность определять макросы, связанные с именами путей, чтобы каждый оператор #include окончательно идентифицировал нужный заголовок, кажется НАМНОГО чище. - person supercat; 07.07.2016

Это работает для VS2013. (Конечно, можно сделать проще.)

#define myIDENT(x) x
#define myXSTR(x) #x
#define mySTR(x) myXSTR(x)
#define myPATH(x,y) mySTR(myIDENT(x)myIDENT(y))

#define myLIBAEdir D:\\Georgy\\myprojects\\LibraryAE\\build\\native\\include\\ //here whitespace!
#define myFile libae.h

#include myPATH(myLIBAEdir,myFile)
person ged    schedule 25.08.2016

Судя по вашему описанию, вы обнаружили, что не каждый "" является строкой. В частности, #include "corefoundation/header.h" выглядит как обычная строка, но это не так. Грамматически директивы препроцессора outside в кавычках предназначены для компилятора и компилируются в строковые литералы с завершающим нулем. Текст в кавычках в директивах препроцессора интерпретируется препроцессором способом, определяемым реализацией.

Тем не менее, ошибка в вашем примере связана с тем, что Boost вставил второй и третий токен: / и filename. Первый, четвертый и пятый токены (directory, . и h) остаются без изменений. Это не то, что вы хотели, очевидно.

Намного проще полагаться на автоматическую конкатенацию строк. "directory/" "filename" — это тот же строковый литерал, что и "directory/filename". Обратите внимание, что между двумя фрагментами нет +.

person MSalters    schedule 18.08.2015
comment
к сожалению, конкатенация строк не происходит в препроцессоре, поэтому в моем конкретном случае это не вариант. - person Richard Hodges; 18.08.2015
comment
@RichardHodges: Ну, технически это зависит от препроцессора IIRC. Но, как я уже сказал, текст в кавычках в директиве пропроцессора не является строкой и не обязательно ведет себя как строка. Если это не строка, то не ожидайте конкатенации строк. - person MSalters; 18.08.2015