Различное поведение fmod (и других) в С++ 11, в Visual Studio, по крайней мере

У меня есть примерный код, который ведет себя по-другому в Visual С++ 2012 с новыми заголовками С++ 11, чем в VС++ 2010. Это касается того, что происходит, когда вы вызываете функцию std:: fmod, которую вы получаете, когда включаете cmath, а когда аргументы, которые вы передаете, не удваиваются, а скорее являются классами, которые имеют неявное преобразование в двойной оператор:

#include <cmath>

class Num {
double d_;
public:
Num(double d) : d_(d) {}

operator double() const { return d_; }
};

int main(int argc, char* argv[]) {
Num n1(3.14159265358979323846264338327950288419716939937510);
Num n2(2.0);

double result1 = fmod((double)n1, (double)n2);
double result2 = fmod((float)n1, (float)n2);
double result3 = fmod(n1, n2);

if (result2==result1) std::cout << "fmod(double, double) returns the same as fmod(float,float)" << std::endl;
if (result3==result1) std::cout << "fmod(Num, Num) returns the same as fmod(double,double)" << std::endl;
if (result3==result2) std::cout << "fmod(Num, Num) returns the same as fmod(float,float)" << std::endl;
}

Скорее всего, это вызывает версию fmod, которая принимает два поплавка, а не версию fmod, которая принимает два удвоения.

Итак, мой вопрос в том, правильно ли это соответствует стандарту С++ 11? Единственная информация, которую я могу найти в поведении, находится в документации cppreference.com здесь, в которой говорится (выделено мной):

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

Однако реализация в заголовочных файлах Visual Studio, по-видимому, реализует "иначе это float".

Кто-нибудь знает, в чем заключается намерение:-)?

Запустив пример через онлайн-версию С++ 11 GCC (у меня нет простого доступа к недавней копии GCC в противном случае), она, по-видимому, вызывает "двойную" версию fmod, что и есть Я наивно ожидаю.

Для ясности я использую

Microsoft (R) C/С++ Оптимизация компилятора версии 17.00.51106.1 для x86

что есть с

Microsoft Visual Studio Express 2012 для Windows Desktop версии 11.0.51106.01 Обновление 1

Ответ 1

Это связано с этим вопросом. Причина в том, что для обеспечения дополнительных перегрузок, требуемых стандартом (и цитируемых в вашем вопросе), VS 2012 определяет общие шаблоны функций для всех математических функций с 2 ​​аргументами. Таким образом, вы на самом деле не называете fmod(float, float), но fmod<Num>(Num, Num).

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

Но фактический фундаментальный тип для вызова функции fmod for для этого определяется по типу этого типа от <xtgmath.h>:

template<class _Ty>
    struct _Promote_to_float
    {   // promote integral to double
    typedef typename conditional<is_integral<_Ty>::value,
        double, _Ty>::type type;
    };

template<class _Ty1,
    class _Ty2>
    struct _Common_float_type
    {   // find type for two-argument math function
    typedef typename _Promote_to_float<_Ty1>::type _Ty1f;
    typedef typename _Promote_to_float<_Ty2>::type _Ty2f;
    typedef typename conditional<is_same<_Ty1f, long double>::value
        || is_same<_Ty2f, long double>::value, long double,
        typename conditional<is_same<_Ty1f, double>::value
            || is_same<_Ty2f, double>::value, double,
            float>::type>::type type;
    };

Что это такое - проверьте продвинутый тип _Promote_to_float (который в вашем случае снова Num, потому что он проверяет, не является ли его интеграл, который Num явно) для всех типов с плавающей запятой, пока он не будет совпадений, которых нет, и, следовательно, приводит к случаю else float.

Причиной этого ошибочного поведения является то, что эти дополнительные математические перегрузки никогда не предназначались для каждого типа, но только для встроенных арифметических типов (и однозначная стандартная формулировка должна быть исправлена, как указано в мой ответ на связанный вопрос). Таким образом, во всем этом методе дедукции, описанном выше, VS 2012 предполагает, что пройденные типы являются либо встроенными интегральными типами, либо, если нет, то встроены типы с плавающей запятой, что, конечно же, не выполняется для Num. Таким образом, фактическая проблема заключается в том, что VS предоставляет слишком общие математические функции, тогда как они должны обеспечивать только перегрузки для встроенных типов (например, правильно выполненные для функций с 1 аргументом). Как указано в связанном ответе, я уже подал на него ошибку.

РЕДАКТИРОВАТЬ: Фактически (как вы поняли), даже если бы они выполняли текущую двусмысленную стандартную формулировку и предоставляли общие шаблоны функций, они все равно должны были определить фактический продвинутый тип из этих общих аргументов как double вместо float. Но я думаю, что настоящая проблема заключается в том, что они полностью игнорируют возможное наличие нестроенных типов в этом процессе преобразования всего типа (поскольку для встроенных типов их логика отлично работает).

Но в соответствии с текущей двусмысленной стандартной формулировкой (которая уже запланирована для изменения) в разделе 26.8 [c.math] они действительно правильно выведут продвинутый тип как float (per 3-й случай):

должны быть дополнительные перегрузки, достаточные для обеспечения:

  • Если любой аргумент, соответствующий двойному параметру, имеет тип long double, то все аргументы, соответствующие двойным параметрам, эффективно отбрасывается в длинный двойной.
  • В противном случае, если любой аргумент, соответствующий двойному параметру, имеет тип double или integer, то все аргументы, соответствующие двойные параметры эффективно дублируются.
  • В противном случае все аргументы, соответствующие двойным параметрам, эффективно применяются для float.

Ответ 2

Как отметил христианин в своем связанном вопросе и ответе, это поведение, которое строгое чтение Стандарта требует.

Однако вы можете легко обойти это для всех версий:

Num fmod(const Num a, const Num b)
{
    const double cvt_a = a;
    const double cvt_b = b;
    return Num(fmod(cvt_a, cvt_b));
}