Hello!
This is my first post in the context of C++ standardization, so I may be missing some obvious things here.
I have found what I would consider a language specification defect.
https://isocpp.org/std/submit-issue directed me to post here (std-discussion) first.
Consider this perfectly fine and working C++98 program:
#include <cmath>
#include <cstdlib>
int main() {
return std::abs(static_cast<short>(23)) % 42;
}
This compiles with all C++98 compilers. In the std::abs(short) call, short gets promoted to int and std::abs(int) is called.
C++11 added, amongst others, the following wording (quoting current C++17 working draft, but the wording had not changed):
http://www.open-std.org/jtc1/sc22/wg21/docs/papers/2016/n4582.pdf P.1083 §26.9.15.2 [c.math] "Otherwise,
if any argument of arithmetic type corresponding to a double argument has type double or an
integer type, then all arguments of arithmetic type corresponding to double arguments are
effectively cast to double."
C++17 draft adds another paragraph:
http://www.open-std.org/jtc1/sc22/wg21/docs/papers/2016/n4582.pdf P.1080 §26.9.10 [c.math] "If abs() is
called with an argument of type X for which is_unsigned<X>::value is true and if X cannot be
converted to int by integral promotion (4.5), the program is ill-formed. [ Note: Arguments that
can be promoted to int are permitted for compatibility with C. — end note ]"
I find it somewhat confusing and probably even contradictory to on the one hand specify abs() in terms of integral promotion in §26.9.10 and on the other hand demand all integral types to be converted to double in §26.9.15.2.
Most compilers I tested appear to not consider §26.9.15.2 for std::abs and compile the code successfully (tested on MSVS 2010-2015, GCC 4.1-4-4, clang 3.0-3.7, all with their own standard library implementation, and in all supported C++ versions (-std=c++98,c++11,c++14,c++1z).
GCC 6.x (tested with 6.1.1 on Debian testing) (and GCC 4.5-5.3 with std::abs but not with ::abs) however implement §26.9.15.2 and fail to compile (again in all std versions, even with -std=c++98 (which is certainly a GCC bug in my opinion)):
manx@vmdebiantesting:~/tmp$ cat test.cpp
#include <cmath>
#include <cstdlib>
int main() {
return std::abs(static_cast<short>(23)) % 42;
}
manx@vmdebiantesting:~/tmp$ g++-6 -std=c++1z -O3 -Wall -Wextra test.cpp
test.cpp: In function ‘int main()’:
test.cpp:4:42: error: invalid operands of types ‘__gnu_cxx::__enable_if<true, double>::__type {aka double}’ and ‘int’ to binary ‘operator%’
return std::abs(static_cast<short>(23)) % 42;
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~^~~~
manx@vmdebiantesting:~/tmp$
As far as I read the standard, GCC's interpretation appears to be valid.
However, as can be seen here, this breaks existing code.
I do acknowledge the reason for the wording and semantics demanded by §26.9.15.2, i.e. being able to call math functions with integral types or with partly floating point types and partly integral types. Converting integral types to double certainly makes sense here for all the other floating point math functions.
However, abs() is special. abs() has overloads for the 3 wider integral types which return integral types. abs() originates in the C standard in stdlib.h and had originally been specified for integral types only. Calling it in C with a short argument returns an int. Calling std::abs(short) in C++98 also returns an int. Calling std::abs(short) in C++11 and later with GCC's interpretation suddenly returns a double.
I know of multiple older code bases (
https://lib.openmpt.org/ [1],
https://www.libreoffice.org/ [2],
http://kicad-pcb.org/ [3] and my own private code base as well) that already got bitten by this particular and/or related problems.
How the standard is meant to be interpreted in this case is not clear. If GCC's interpretation is considered to be valid or even the only correct one, I very much consider this a language defect as it clearly breaks existing and working code.
In its current (since C++11 / GCC) state, std::abs is also practically unusable in generic code because std::abs(std::int32_t) returns int or long and std::abs(std::int16_t) returns double, which is rather unintuitive and very confusing.
With GCC 6, ::abs() (i.e. C abs()) is even unusable in inline functions or macros included via C headers:
manx@vmdebiantesting:~/tmp$ cat somelib.h
#ifndef SOMELIB_H
#define SOMELIB_H
#include <math.h>
#include <stdlib.h>
#ifdef __cplusplus
extern "C" {
#endif
static inline short foo(short x) {
return abs(x) % 42;
}
#ifdef __cplusplus
}
#endif
#endif
manx@vmdebiantesting:~/tmp$ cat test2.cpp
#include "somelib.h"
int main() {
return foo(23);
}
manx@vmdebiantesting:~/tmp$ cat test3.c
#include "somelib.h"
int main() {
return foo(23);
}
manx@vmdebiantesting:~/tmp$ g++-6 -std=c++1z -O3 -Wall -Wextra test2.cpp
In file included from test2.cpp:1:0:
somelib.h: In function ‘short int foo(short int)’:
somelib.h:9:16: error: invalid operands of types ‘__gnu_cxx::__enable_if<true, double>::__type {aka double}’ and ‘int’ to binary ‘operator%’
return abs(x) % 42;
~~~~~~~^~~~
manx@vmdebiantesting:~/tmp$ gcc-6 -std=c99 -O3 -Wall -Wextra test3.c
manx@vmdebiantesting:~/tmp$
With the increasing popularity of single-header C libraries like
https://github.com/nothings/stb/ , this will affect probably even more code in the future and/or force C programmers to work around bugs in potentially future versions of the standard of another language (C++) which generally aims to be somewhat compatible with C (and had been in the past in this particular aspect).
Additionally, I fail to see a reason why this change in semantics of std::abs(short) could have been intentional in the first place.
Based on the assumption that my analysis is correct, in my opinion, the standard should at the very very least be clarified about what the semantics and return type of std::abs(short) should be (as apparently GCC's interpretation differs from MSVC's and clang's). If the standard demands ::abs and std::abs to behave identically (which I did not investigate thoroughly), it seems unavoidable to revert to C++98 (with addition of long long) semantics for std::abs in order to keep (or restore) compatibility with C (Note: We are not talking about core language features here where there are certainly valid points to not supporting every new C language feature in C++, but instead about a library API incompatibility. Compatibility is way easier to achieve there.).
Note, the situation for signed char is the same as the situation for short.
Note, §26.9.15.2 totally makes sense for std::fabs but not for std::abs, thus a possible solution may be to explicitly exempt std::abs in that paragraph. Another possible solution might be to explicitly demand integral promotion for narrower integral types somewhere near §26.9.10 for std::abs (i.e. by specifying overloads for signed char and short which cast to int).
Note, in a more complicated expression, where std::abs(short) is called somewhere near the leafs of the AST and further calculations are performed on the result without any explicit casting, the change in behaviour can cause potentially almost whole expression to be evaluated as double instead of as int. If the user is unlucky, even without any compiler warning. Even the computed value of the whole expression might change due to rounding/truncation. Running such code on a platform without an FPU can, in addition to the semantic problems, also lead to disastrous performance regressions.
Note, further discussions related to this issue are also available on the net at [4].
Note, as far as I can see, the proposed or implemented solutions for
https://cplusplus.github.io/LWG/lwg-active.html#2294 ,
https://cplusplus.github.io/LWG/lwg-defects.html#2192 and/or
https://cplusplus.github.io/LWG/lwg-defects.html#2086 will not resolve this issue.
Questions:
Was this change in behaviour intentional?
If so, why?
Must/should/may/must-not ::abs() and std::abs() behave identically?
Should it be considered a bug in the C++ standard?
Should it be considered as a bug in GCC's libstdc++?
Should I proceed reporting this issue as outlined in
https://isocpp.org/std/submit-issue ?
If so, are there any opinions on how exactly the issue should be resolved?
Regards,
Jörn
Footnotes:
[1]
https://source.openmpt.org/browse/openmpt/trunk/OpenMPT/soundlib/tuning.cpp?op=revision&rev=6295&peg=6295 (please note that the commit message and comment are not accuratly reflecting the issue here, this was written before a more thorough analysis had been done)
https://source.openmpt.org/browse/openmpt/trunk/OpenMPT/soundlib/tuning.cpp?op=diff&rev=6295&peg=6295 (please note that the commit message and comment are not accuratly reflecting the issue here, this was written before a more thorough analysis had been done)
https://source.openmpt.org/browse/openmpt?manualorder=1&op=comp&compare[0]=%2Ftrunk%2FOpenMPT%2Fcommon%2Fmisc_util.h&compare_rev[0]=6298&compare[1]=%2Ftrunk%2FOpenMPT%2Fcommon%2Fmisc_util.h&compare_rev[1]=6313&comparesubmit=Compare+Paths[2]
https://gerrit.libreoffice.org/#/c/22050/[3]
These are more related to issue 2192 and 2086 then to this particular problem, but listed here in order to provide a better overview.
https://lists.launchpad.net/kicad-developers/msg21674.htmlhttps://bazaar.launchpad.net/~kicad-product-committers/kicad/product/revision/5831https://bazaar.launchpad.net/~kicad-product-committers/kicad/product/revision/6350[4]
https://stackoverflow.com/questions/8226705/g-abs-on-a-short-int-appears-to-turn-it-into-a-doublehttps://gcc.gnu.org/ml/libstdc++/2016-02/msg00001.html