Is it valid to pass non-arithmetic types as arguments to cmath functions?

389 views Asked by At

Given the following user-defined type S with a conversion function to double:

struct S
{
   operator double() { return 1.0;}
};

and the following calls to cmath functions using the type S:

#include <cmath>

void test(S s) {
   std::sqrt(s); 
   std::log(s); 
   std::isgreater(s,1.0);
   std::isless(s,1.0);
   std::isfinite(s) ;
}

This code compiles with gcc using libstdc++ (see it live) but with clang using libc++ it generates errors for several of the calls (see it live) with the following error for isgreater:

error: no matching function for call to 'isgreater'
   std::isgreater(s,1.0);
   ^~~~~~~~~~~~~~

note: candidate template ignored: disabled by 'enable_if' [with _A1 = S, _A2 = double]
std::is_arithmetic<_A1>::value &&
^

and similar errors for isless and isfinite, so libc++ expects the arguments for those calls to be arithmetic types which S is not, we can confirm this by going to the source for libc++ cmath header. Although, the requirement for arithmetic types is not consistent across all the cmath functions in libc++.

So the question is, is it valid to pass non-arithmetic types as arguments to cmath functions?

1

There are 1 answers

4
Shafik Yaghmour On BEST ANSWER

TL;DR

According to the standard it is valid to pass non-arithmetic types as arguments to cmath functions but defect report 2068 argues the original intent was that cmath functions should be restricted to arithmetic types and it appears possible using non-arithmetic arguments will eventually be made ill-formed. So although technically valid using non-arithmetic types as arguments seems questionable in light of defect report 2068.

Details

The cmath header is covered in the draft standard section 26.8 [c.math] provides an additional float and long double overload for the each function defined in math.h that takes a double argument and further, paragraph 11 provides for sufficient overloads and says:

Moreover, there shall be additional overloads sufficient to ensure:

  1. If any argument corresponding to a double parameter has type long double, then all arguments corresponding to double parameters are effectively cast to long double.
  2. Otherwise, if any argument corresponding to a double parameter has type double or an integer type, then all arguments corresponding to double parameters are effectively cast to double.
  3. Otherwise, all arguments corresponding to double parameters are effectively cast to float.

This seems valid in C++11

In C++11 section 26.8 [c.math] does not include any restrictions disallowing non-arithmetic arguments to cmath functions. In each case from the question we have an overload available which takes double argument(s) and these should be selected via overload resolution.

Defect report 2086

But for C++14 we have defect report 2086: Overly generic type support for math functions, which argues that the original intent of section 26.8 [c.math] was to limit cmath functions to be valid only for arithmetic types, which would mimic how they worked in C:

My impression is that this rule set is probably more generic as intended, my assumption is that it is written to mimic the C99/C1x rule set in 7.25 p2+3 in the "C++" way [...] (note that C constraints the valid set to types that C++ describes as arithmetic types, but see below for one important difference) [...]

and says:

My current suggestion to fix these problems would be to constrain the valid argument types of these functions to arithmetic types.

and reworded section 26.8 paragraph 11 to say (emphasis mine):

Moreover, there shall be additional overloads sufficient to ensure:

  1. If any arithmetic argument corresponding to a double parameter has type long double, then all arithmetic arguments corresponding to double parameters are effectively cast to long double.
  2. Otherwise, if any arithmetic argument corresponding to a double parameter has type double or an integer type, then all arithmetic arguments corresponding to double parameters are effectively cast to double.
  3. Otherwise, all arithmetic arguments corresponding to double parameters are effectively cast to are effectively cast to have type float.

So this is invalid in C++14?

Well, despite the intent it looks technically to still be valid as argued in this comment from the discussion in libc++ bug report: incorrect implementation of isnan and similar functions:

That may have been the intent, but I don't see any way to read the standard's wording that way. From the example in comment#0:

std::isnan(A());

There are no arguments of arithmetic type, so none of the bullets in 26.8/11 apply. The overload set contains 'isnan(float)', 'isnan(double)', and 'isnan(long double)', and 'isnan(float)' should be selected.

So, the rewording by DR 2086 of paragraph 11 does not make it ill-formed to call the float, double and long double overloads available otherwise with non-arithmetic arguments.

Technically valid but questionable to use

So although the C++11 and C++14 standard do not restrict cmath functions to arithmetic arguments DR 2068 argues the intent of 26.8 paragraph 11 was to restrict cmath functions to take only arithmetic arguments and apparently intended to close the loophole in C++14, but did not provide strong enough restrictions.

It seems questionable to rely on a feature which could become ill-formed in a future version of the standard. Since we have implementation divergence any code that relies on passing non-arithmetic arguments to cmath functions for those cases is non-portable and so will be useful only in limited situations. We have an alternative solution, which is to explicitly cast non-arithmetic types to arithmetic types, which bypasses the whole issue, we no longer have to worry about the code becoming ill-formed and it is portable:

std::isgreater( static_cast<double>(s) ,1.0)
                ^^^^^^^^^^^^^^^^^^^^^^

As Potatoswatter points out using unary + is also an option:

std::isgreater( +s ,1.0)

Update

As T.C. points out in C++11 it can be argued that 26.8 paragraph 11 bullet 3 applies since the argument is neither long double, double nor an integer and should therefore the arguments of type S should be cast to float first. Note, as indicated by the defect report gcc never implemented this and as far I know neither did clang.