Статья описывает некоторые затруднения, которы мы встретили при попытке адаптации одного из наших старых Windows-only проектов (плагин к MT4 серверу) к кросскомпиляции под Linux (CI, статический анализ, автотесты и прочие модные слова). Точнее, в коде присутствовал ряд конструкций, которые спокойно съедались MSVC, но категорически отказывались компилироваться с использованием mingw/gcc.


image


Под катом 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)


  1. Antervis
    22.03.2017 10:19
    +8

    Более правильное (рабочее) решение:

    extern «C» __declspec(dllexport) void SomeMethod() {}

    А еще более правильное — использовать вместо __declspec(dllexport) макрос, который развернется в __declspec(dllexport)/__declspec(dllimport) на винде в зависимости от того, используется хедер для сборки или линковки библиотеки, или в __attribute__((visibility(«default»))) в gcc


    1. Mingun
      22.03.2017 20:06

      Лично я вообще удивлён, что кто-то пишет такое напрямую, без макроса. По-моему макрос — это уже стандарт де-факто.


      1. Antervis
        22.03.2017 20:33
        +3

        костыль это, ставший «стандартом» в силу отсутствия альтернатив


        1. Mingun
          22.03.2017 23:01

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


          1. orcy
            23.03.2017 10:47

            Есть идеи по добавлению: https://stdcpp.ru/proposals/feb5244f-f6a9-4cc0-ae30-f6b549d2d6c9


  1. 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 без всяких предупреждений и это бесит.


    1. FlexFerrum
      22.03.2017 14:17
      +1

      То же самое с ключевым словом template в аналогичных ситуациях.


  1. 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