OCCI (Oracle C++ Call Interface) — достаточно удобная объектно-ориентированная оболочка над OCI (Oracle Call Interface), позволяющая подключаться и работать с СУБД Oracle из C++ практически с той же легкостью, какую обеспечивают интерпретируемые языки программирования вроде Perl. Однако, OCCI имеет и свои недостатки. В частности, числа в Oracle представлены типом Number, обеспечивающим максимальную точность до 38 значащих цифр, но в OCCI нет метода, позволяющего преобразовать этот тип в 64-битное целое число. Есть возможность преобразовывать в обычное целое число (32 бита), в double (число с плавающей точкой, 64 бита) и long double (число с плавающей точкой, 80 бит (по стандарту С)), но не в 64-битное целое.

Вариант с преобразованием в double и последующим преобразованием в long long может устроить многих пользователей, но далеко не всех, так как он обеспечивает только 52-битную точность (хранимая мантисса числа в типе double имеет размер 52 бита) и, как следствие, возможную потерю точности при больших числах. Казалось бы, проблему могло бы решить промежуточное преобразование в long double, но здесь в игру вступает ограничение компилятора от Microsoft — он не поддерживает 80-битные числа с плавающей точкой и воспринимает тип long double аналогом типа double.

В то же время OCI, лежащий в основе OCCI, проводить преобразование из Number в long long умеет. Для этого в нем есть следующая функция:

sword OCINumberToInt (OCIError *err, const OCINumber *number, uword rsl_length, uword rsl_flag, void *rsl);

Функция принимает в параметрах указатель на исходное число const OCINumber *number, на результирующее число void *rsl, а также размер результирующего числа в байтах uword rsl_length. И среди поддерживаемых размеров результата есть 8, т.е. 64 бита. Теперь остается только получить OCINumber.

К сожалению, здесь мы сталкиваемся со следующей проблемой — в OCCI из ResultSet (класс, являющийся результатом выполнения запроса) получить OCINumber также возможности нет, и класс oracle::occi::Number, представляющий собой оболочку над OCINumber, тоже не предоставляет доступа к своим внутренностям, объявив их как private:

private:
  /* Private constructor for constructing number from methods inside  */
  OCINumber getOCINumber() const;

  OCINumber data;

И вот здесь на помощь нам приходит возможность преобразования совершенно разных типов друг к другу в C++. Поскольку класс Number не имеет виртуальных методов, а член OCINumber data является первым в классе, адрес этого члена в памяти совпадает с адресом экземпляра класса! Т.е. нам достаточно написать:

oracle::occi::Number number;
OCINumber *ociNumber = (OCINumber*)&number;

В результате функция преобразования Number в 64-битное целое число (для MSVC) выглядит следующим образом:

__int64 OCCINumberToInt64 (const oracle::occi::Number &number, OCIError *hError, bool bSigned)
{
	const OCINumber *ociNumber = (const OCINumber*)&number;
	__int64 result;
	OCINumberToInt (hError, ociNumber, 8, bSigned ? OCI_NUMBER_SIGNED : OCI_NUMBER_UNSIGNED, &result);

	return result;
}

А чтобы получить необходимый функции OCI Error Handle, можно воспользоваться следующим кодом:

oracle::occi::Environment env;
...
OCIEnv *hEnv = env->getOCIEnvironment ();
OCIError *hError = 0;
OCIHandleAlloc (hEnv, (void**)&hError, OCI_HTYPE_ERROR, 0, 0);

...

OCIHandleFree (hError, OCI_HTYPE_ERROR);

Комментарии (17)


  1. monah_tuk
    28.05.2015 17:56
    +2

    Опасно. Использовать и молиться, что бы ничего в классе не изменилось.


    1. phprus
      28.05.2015 18:07
      +1

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


    1. kdekaluga Автор
      28.05.2015 19:35

      Опасность есть, согласен. Лично для себя (в MSVC12) я использую для проверки вот такой код (его можно вставить в место инициализации программы):

      	#ifdef _DEBUG
      	DWORD dataOffset;
      	__asm
      	{
      		mov		eax, oracle::occi::Number::data
      		mov		[dataOffset], eax
      	}
      
      	ASSERT (dataOffset == 0);
      	#endif
      

      Дело в том, что студия разрешает ассемблерному коду свободный доступ к private членам класса. Таким образом я получаю смещение члена data, а затем проверяю, что оно равно нулю.
      Код так же не дает 100% гарантии — если вдруг в будущей версии под именем data будет совсем другой член. Но это маловероятно.


      1. RPG18
        28.05.2015 19:58

        В отдельной единице трансляции:

        #define private public
        


        1. phprus
          28.05.2015 20:12

          Так как там есть приватные функции эта замена сломает ABI.


          1. RPG18
            28.05.2015 22:39

            Можно поподробней, почему сломается?


            1. phprus
              29.05.2015 07:08

              techbase.kde.org/Policies/Binary_Compatibility_Examples#Change_the_access_rights
              Может изменится декорированное имя метода (а в MSVC точно изменится).

              А вообще, про ABI C++-библиотек есть хорошая документация от KDE: techbase.kde.org/Policies/Binary_Compatibility_Issues_With_C++


              1. RPG18
                29.05.2015 09:29

                В С++ есть принцып: не платим за то, что не используем. На этом принципе основаны различные проверки во время компиляции, примеры можно почитать у того же Александреску.

                Если мы объявляем функцию

                OCINumber* extractOCINumber(oracle::occi::Number& );
                

                а реализацию изолируем в отдельной еденице трансляции extractOCINumber.cpp
                #define private public
                // подключаем необходимый заголовочный файл
                OCINumber* extractOCINumber(oracle::occi::Number& foo)
                {
                  return &foo.data;
                }
                //и ничего более
                

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


                1. monah_tuk
                  29.05.2015 09:43

                  после:

                  //и ичего более
                  

                  я бы добавил:
                  #undef private
                  

                  :-)


                1. phprus
                  29.05.2015 22:17

                  Да, в таком случае если в этой единице трансляции ничего из методов класса oracle::occi::Number вызываться не будет, то ничего сломаться не должно.
                  Кажется я немного погорячился жесткостью требований для полей. Но для вызова приватных методов это уже не сработает.


        1. kdekaluga Автор
          28.05.2015 20:54

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


      1. monah_tuk
        29.05.2015 03:46

        На уровне асма и машинных кодов различий между public/private/protected нет, это асбтракции для человека. Но этим ещё и переносимость ломается. Но под задачу.


  1. Yury
    29.05.2015 00:57
    +1

    Кроме перечисленного, Number можно еще преобразовать в строку (toText) или массив (toBytes). Ну а потом уже в int64_t или что-то еще. Да, это медленнее, но зато безопасно.


    1. monah_tuk
      29.05.2015 04:13

      Лучше уж так. А потом профилировщиком посмотреть реально узкие места.


  1. monah_tuk
    29.05.2015 04:16

    Ещё есть проблема в

    __int64 OCCINumberToInt64 (const oracle::occi::Number &number, OCIError *hError, bool bSigned)
    {
        const OCINumber *ociNumber = (const OCINumber*)&number;
        __int64 result;
        OCINumberToInt (hError, ociNumber, 8, bSigned ? OCI_NUMBER_SIGNED : OCI_NUMBER_UNSIGNED, &result);
    
        return result;
    }
    

    вы тут сохраняете в знаковый int64 как знаковые, так и беззнаковые значения. Судя по всему тестировали только на положительных числах?

    Возможно стоило бы переписать как-то так (C++11 доступен? Хотя даже без него можно)
    // в случае C++11
    #include <type_traits>
    
    template<typename T>
    T OCCINumberToInt(const oracle::occi::Number &number, OCIError *hError)
    {
        // тут можно нанаставлять статик ассертов для лимитации типов по размерам
    
        const OCINumber *ociNumber = reinterpret_cast<const OCINumber*>(&number);
        T result;
        // Если C++11
        //const static bool bSigned = std::is_signed<T>::value;
        // Если нет:
        const static bool bSigned = T(-1) < T(0);
        OCINumberToInt (hError, ociNumber, sizeof(T), bSigned ? OCI_NUMBER_SIGNED : OCI_NUMBER_UNSIGNED, &result);
        return result;
    }
    


    bSigned тут вычислится один раз для каждого типа.

    тогда использовать можно так:
    uint64_t unsignedValue64 = OCCINumberToInt<uint64_t>(ociNumber, err);
    int64_t signedValue64 = OCCINumberToInt<int64_t>(ociNumber, err);
    

    причём не только для 64бит, но внимательно посмотреть и статических ассертов добавить на допустимые длины.

    кроме того, я бы не стал использова Си-стиль приведения типов, а использовал бы громкий reinterpret_cast<>().


    1. kdekaluga Автор
      29.05.2015 07:59

      вы тут сохраняете в знаковый int64 как знаковые, так и беззнаковые значения. Судя по всему тестировали только на положительных числах?

      Справедливое замечание. В моей задаче полученный int64 сравнивается с другими int64 только на равно/не равно, а потом сохраняется в двоичном виде в файл, который читает другой модуль. Поэтому для меня имеет значение только как проведет операцию преобразования OCI — со знаком или без, а это я передаю ему параметром. Но для «библиотечной» функции ваш вариант, конечно, правильней.
      причём не только для 64бит, но внимательно посмотреть и статических ассертов добавить на допустимые длины.

      Для чисел меньших размеров необходимости в такой функции нет, т.к. сам Number имеет соответствующие операторы преобразования в 1-, 2- и 4-байтные знаковые и беззнаковые целые числа. Проблема именно в отсутствии поддержки 64-битных значений.
      bSigned тут вычислится один раз для каждого типа.

      А для чего вы определили ее вообще? Можно ведь написать так:

      OCINumberToInt (hError, ociNumber, sizeof(T), T(-1) < T(0) ? OCI_NUMBER_SIGNED : OCI_NUMBER_UNSIGNED, &result);
      


      1. monah_tuk
        29.05.2015 09:40

        А для чего вы определили ее вообще? Можно ведь написать так:

        можно. Но для наглядности я бы вообще так сделал:
        const static bool bSigned = T(-1) < T(0);
        const static uword flags    = bSigned ? OCI_NUMBER_SIGNED : OCI_NUMBER_UNSIGNED;
        // в случае C++11 можно и одной строчкой: из контекста и так понятно что происходит, а что такое T(-1) < T(0) сто пудов не сходу вспомнится:
        //constexpr static uword flags = std::is_signed<T>::value ? OCI_NUMBER_SIGNED : OCI_NUMBER_UNSIGNED;
        ...
        OCINumberToInt (hError, ociNumber, sizeof(T), flags, &result);
        


        просто читабельность повыше. Оптимизаторы нынче умные. Можешь пометить как constexpr (C++11) и, я думаю, оно в компайл-тайме посчитается (а может и так соптимизируется). А код разбирать потом зело удобнее будет.