Статья описывает некоторые затруднения, которы мы встретили при попытке адаптации одного из наших старых Windows-only проектов (плагин к MT4 серверу) к кросскомпиляции под Linux (CI, статический анализ, автотесты и прочие модные слова). Точнее, в коде присутствовал ряд конструкций, которые спокойно съедались MSVC, но категорически отказывались компилироваться с использованием mingw/gcc.
Под катом 7 наиболее часто встретившихся примеров кода, которые будут компилироваться MSVC, но не будут с gcc, и способы это лечить.
Дисклеймер
Цель статьи – не сказать, что какой-то компилятор лучше, чем другие, а указать на некоторые проблемы, которые могут возникнуть при адаптации кода к другим компиляторам (особенно если до этого использовался только MSVC). Также некоторые (если не все) элементы поведения можно свести к одному, если подкрутить флаги компиляции, но ведь лучше все-таки поправить код (хотя бы и sed'ом), правда?
Условия задачи
Имеем среднего размера проект (около 15к SLOC не считая библиотек), в котором используется CMake с практически дефолтными флагами компиляции. MSVC используем 14 версии, а mingw-gcc — 6.3.
Найденные проблемы
Декорация имен методов
Внутри нашего проекта присутствует несколько методов, которые должны вызываться как C методы для того, чтобы плагин распознавался сервером. Оригинально в коде использовались следующие конструкции:
__declspec(dllexport) void SomeMethod() {}
При компиляции gcc имя функции декорировалось, что приводило к тому, что сервер не определял метод в плагине. Более правильное (рабочее) решение:
extern "C" __declspec(dllexport) void SomeMethod() {}
Пути к файлам и include
Различие разделителей в путях на разных системах также приводит к ошибкам на этапе компиляции. Код
#include "directory\\include.h"
откажется компилироваться под Linux/gcc, хотя под Windows/MSVC никаких проблем не будет. Это не совсем ошибка, но следует отметить, что для удобства переносимости лучше все же использовать обычный слэш, поскольку он воспринимается большинством систем. С путями также есть и другая проблема...
Регистр и include
Как вы, вероятно, знаете, пути some/path
и SoMe/pATh
в Windows не различаются, но это не так в некоторых других системах, что приводило к ошибкам, если программист указывал путь в заголовочному файлу без учета регистра. Например:
#include <Winsock2.h>
выдаст ошибку с gcc под Linux, потому что указанный файл просто не будет найден. Аналогичная проблема также наблюдается с именами библиотек, например, Ws2_32
против ws2_32
.
Как определить целевую платформу
В проекте активно используется QuickFIX, который, как предполагается, должен компилироваться и работать под разными системами. В актуальной версии QuickFIX используются следующие конструкции:
#ifndef _MSC_VER
#include <unistd.h>
#endif
Не надо так делать. При использовании mingw _MSC_VER
не определяется, вместо этого правильнее проверять _WIN32
для определения целевой платформы, а _MSC_VER
использовать, только если вы хотите включить код, специфичный для MSVC.
Pure virtual методы
Код
class SomeClass
{
virtual void someMethod() = NULL;
};
при попытке компиляции gcc радостно скажет
invalid pure specifier (only «= 0» is allowed)
но не вызовет ошибок у MSVC. Причина проста: gcc раскрывает макрос NULL
не в 0, а в __null
(что, в общем-то, совсем не запрещено). Решение: очевидно, отказаться от использования NULL
для указания pure virtual методов и использовать = 0
.
Определение методов внутри заголовочного файла
Код
class SomeClass
{
SomeClass::SomeClass() {};
};
при использовании gcc выдаст
extra qualification ‘SomeClass::’ on member ‘SomeClass’
Правильный ответ, очевидно, не должен содержать SomeClass::
. Вообще, в драфте стандарта C++14 (параграф 8.3) написано, что:
the declaration shall refer to a previously declared member of the class or namespace to which the qualifier refers
Декларация переменной без указания переменной
Код, написанный с использованием клипбордного интерфейса
void someMethod()
{
SomeClass;
SomeClass class;
}
содержит в себе ошибку, которая игнорируется MSVC, но вызовет ошибку на этапе компиляции у gcc:
declaration does not declare anything
Не уверен, какое поведение здесь правильное, а MSVC, вероятно, просто вырезает неиспользуемую строчку без подачи каких либо сигналов.
Вместо послесловия
Большинство перечисленных мной ошибок, очевидно, довольно легко правятся и без чтения данной статьи, руководствуясь исключительно замечаниями компилятора. Однако она наглядно иллюстрирует различие «двух миров» в восприятии вашего исходного кода, и то, что для вас может быть естественным, отнюдь не является таковым при смене компилятора.
Комментарии (8)
iroln
22.03.2017 11:10+5Ещё MSVC проглатывает отсутствие ключевого слова
typename
в некоторых случаях, а GCC ругается:
need ‘typename’ before ‘A<T*>::obj’ because ‘A<T*>’ is a dependent scope
Например, есть у нас шаблонный класс:
template<typename T> class A { typedef SmartPointer<A> Pointer; };
И мы его используем где-то в шаблонном классе:
template<typename T> class B { typename A<T>::Pointer m_A; // MSVC не ругается на отсутствие typename };
MSVC это проглотит без указания
typename
без всяких предупреждений и это бесит.
gelvaos
22.03.2017 15:30+3Еще можно добавить про способы упаковки структур.
__attribute__ ((packed))
из gcc, не работет в Visual C++, а аналог делается через pragma. Поэтому в нашем проекте пришлось городить что типа вот этого:
# if defined(_MSC_VER) /* __pragma() is specified starting from Visual Studio 2008*/ # if (_MSC_VER < 1500) # error "Unsupport Visual C compiler version. Minimum version is Visual Studio 2008." # endif # define ATTRIBUTE_PACKED /* Enable packing and supress warning C4103: Packing was changed after the inclusion of the header, probably missing #pragma pop */ # define BEGIN_ATTRIBUTE_PACKED __pragma(pack(push,1)) __pragma(warning(disable : 4103)) /* Disable packing and enable warning C4103 back */ # define END_ATTRIBUTE_PACKED __pragma(pack(pop)) __pragma(warning(default : 4103)) # define ATTRIBUTE_SECTION_GCC(x) # elif defined (__GNUC__) # define BEGIN_ATTRIBUTE_PACKED # define END_ATTRIBUTE_PACKED # if defined(__clang__) # define ATTRIBUTE_PACKED __attribute__ ((packed)) # else # define ATTRIBUTE_PACKED __attribute__ ((gcc_struct,packed)) # endif # endif /* defined(_MSC_VER) */
При это стурктуры потом приходится определять вот таким образом:
BEGIN_ATTRIBUTE_PACKED struct s1 ATTRIBUTE_PACKED { ... } struct s2 ATTRIBUTE_PACKED { ... } END_ATTRIBUTE_PACKED
Antervis
А еще более правильное — использовать вместо __declspec(dllexport) макрос, который развернется в __declspec(dllexport)/__declspec(dllimport) на винде в зависимости от того, используется хедер для сборки или линковки библиотеки, или в __attribute__((visibility(«default»))) в gcc
Mingun
Лично я вообще удивлён, что кто-то пишет такое напрямую, без макроса. По-моему макрос — это уже стандарт де-факто.
Antervis
костыль это, ставший «стандартом» в силу отсутствия альтернатив
Mingun
Тем не менее, это фактический стандарт. Каким бы костыльным он ни был. Как вы сами заметили — альтернатив-то нет. Тем более меня удивляет его отсутствие, тем более в студии, где наверняка это уже где-нибудь в шаблонах кода прописано.
orcy
Есть идеи по добавлению: https://stdcpp.ru/proposals/feb5244f-f6a9-4cc0-ae30-f6b549d2d6c9