12

We have an algorithm library doing lots of std::min/std::max operations on numbers that could be NaN. Considering this post: Why does Release/Debug have a different result for std::min?, we realised it's clearly unsafe.

Is there a way to prevent developers from using std::min/std::max?

Our code is compiled both with VS2015 and g++. We have a common header file included by all our source files (through /FI option for VS2015 and -include for g++). Is there any piece of code/pragma that could be put here to make any cpp file using std::min or std::max fail to compile?

By the way, legacy code like STL headers using this function should not be impacted. Only the code we write should be impacted.

Community
  • 1
  • 1
jpo38
  • 20,821
  • 10
  • 70
  • 151
  • Even if it is UB (as the function are in `std`), you can mark the function as *deprecated*. – Jarod42 Oct 10 '16 at 07:23
  • 21
    I guess your approach is understandable but not optimal: Using NaN in arithmetic operations, i.e. for more than just a flag/result is a fault in your design. I would either check in the algorithms, use a own NaN, try to prevent using it or redesign the library. But prevent developers from using standard library tools is born to fail and IMHO riding a dead horse. – Superlokkus Oct 10 '16 at 07:27
  • @Jarod42: How do you do that? – jpo38 Oct 10 '16 at 07:28
  • @jpo38: Look at [c-mark-as-deprecated](http://stackoverflow.com/questions/295120/c-mark-as-deprecated) – Jarod42 Oct 10 '16 at 07:31
  • @Jarod42: Saw that, but could not make it work with a template function being in a namespace (like std::min). – jpo38 Oct 10 '16 at 07:37
  • are min/max where there are NaNs intended to be defined? In any case (ab) ? a: b will return b but (a – CashCow Oct 10 '16 at 07:42
  • @jpo38: Like [that](http://coliru.stacked-crooked.com/a/5ff40f20899dd902). – Jarod42 Oct 10 '16 at 08:16
  • @Jarod42: Nice. Thanks for the help! It could make sense to post this as an answer. – jpo38 Oct 10 '16 at 08:26
  • What about providing nanmin and nanmax and educating your developers to use those when necessary? – csiz Oct 10 '16 at 16:57
  • @csiz: That's what I'm ending up with.... – jpo38 Oct 10 '16 at 18:24
  • I've alvays thought that silent NaNs should just fall through, like so: `return a < b || isnan(a) ? a : b`. Will your allgorithms fail if you overload min/max for used types to let NaNs fall through? – Eugene Ryabtsev Oct 11 '16 at 09:04
  • @EugeneRyabtsev: Depends, each call to min/max has to be checked because some may want to return the nan value, others the non nan value... – jpo38 Oct 11 '16 at 09:19

9 Answers9

32

I don't think making standard library functions unavailable is the correct approach. First off, NaN are a fundamental aspect of how floating point value work. You'd need to disable all kinds of other things, e.g., sort(), lower_bound(), etc. Also, programmers are paid for being creative and I doubt that any programmer reaching for std::max() would hesitate to use a < b? b: a if std::max(a, b) doesn't work.

Also, you clearly don't want to disable std::max() or std::min() for types which don't have NaN, e.g., integers or strings. So, you'd need a somewhat controlled approach.

There is no portable way to disable any of the standard library algorithms in namespace std. You could hack it by providing suitable deleted overloads to locate uses of these algorithms, e.g.:

namespace std {
    float       max(float, float) = delete;             // **NOT** portable
    double      max(double, double) = delete;           // **NOT** portable
    long double max(long double, long double) = delete; // **NOT** portable
    // likewise and also not portable for min
}
Dietmar Kühl
  • 150,225
  • 13
  • 225
  • 380
  • This `delete` trick works perfectly, at least for VS compiler. std::min/std::max can't be used anymore! Not sure I'll use it in the end (as commented by many, it's probably better to avoid comparing NaN to non-NaN anyway). – jpo38 Oct 10 '16 at 07:59
  • 3
    This is pretty much relying on undefined behaviour though. – Hatted Rooster Oct 10 '16 at 08:00
  • may I inquire why it's not portable? – Alexey Andronov Oct 10 '16 at 08:54
  • 8
    @sfk92fksdf: due to 17.6.4.2.1 [namespace.std] paragraph 1: "The behavior of a C++ program is undefined if it adds declarations or definitions to namespace std or to a namespace within namespace std unless otherwise specified. ..." There isn't a specification allowing overloads. The basic reason is simply that the standard library may use these functions and having user code use a different definition is a violation of the One Definition rule. – Dietmar Kühl Oct 10 '16 at 08:58
  • 1
    @DietmarKühl: The complaint arises from Debug and Release having different behaviors. The programmer who typed its inline conditional form won't have that particular problem. – Joshua Oct 10 '16 at 15:32
  • 2
    @Joshua The problem in the other question was that the compiler decided to switch the comparison... I don't see why the compiler couldn't do so even with the inline conditional... or is there something preventing the compiler from changing `x < y` to `y >= x` (for floats)? – Bakuriu Oct 10 '16 at 15:34
  • @Bakuriu: Don't vary your optimization flags between debug and release! I've gotten burned one time too many. – Joshua Oct 10 '16 at 15:36
  • @Bakuriu: I'd hope that compiler writers know that `<` doesn't yield a strict weak order over all values of floating point types. The operator is a strict weak order only when excluding NaNs. – Dietmar Kühl Oct 10 '16 at 15:37
18

I'm going a bit philosophical here and less code. But I think the best approach would be to educate those developers, and explain why they shouldn't code in a specific way. If you'll be able to give them a good explanation, then not only will they stop using functions that you don't want them to. They will be able to spread the message to other developers in the team.

I believe that forcing them will just make them come up with work arounds.

TCDutchman
  • 41
  • 11
David Gatti
  • 3,576
  • 3
  • 33
  • 64
  • 3
    This. Plus a good workflow, ie one that includes code review at pull-request (ex: what is on Github / Bitbucket / ...), this should remove most of the problems here – Synxis Oct 10 '16 at 15:58
7

As modifying std is disallowed, following is UB, but may work in your case. Marking the function as deprecated:

Since c++14, the deprecated attribute:

namespace std
{
    template <typename T>
    [[deprecated("To avoid to use Nan")]] constexpr const T& (min(const T&, const T&));
    template <typename T>
    [[deprecated("To avoid to use Nan")]] constexpr const T& (max(const T&, const T&));
}

Demo

And before

#ifdef __GNUC__
# define DEPRECATED(func) func __attribute__ ((deprecated))
#elif defined(_MSC_VER)
# define DEPRECATED(func) __declspec(deprecated) func
#else
# pragma message("WARNING: You need to implement DEPRECATED for this compiler")
# define DEPRECATED(func) func
#endif

namespace std
{
    template <typename T> constexpr const T& DEPRECATED(min(const T&, const T&));
    template <typename T> constexpr const T& DEPRECATED(max(const T&, const T&));

}

Demo

Jarod42
  • 203,559
  • 14
  • 181
  • 302
4

There's no portable way of doing this since, aside from a couple of exceptions, you are not allowed to change anything in std.

However, one solution is to

#define max foo

before including any of your code. Then both std::max and max will issue compile-time failures.

But really, if I were you, I'd just get used to the behaviour of std::max and std::min on your platform. If they don't do what the standard says they ought to do, then submit a bug report to the compiler vendor.

Bathsheba
  • 231,907
  • 34
  • 361
  • 483
  • Tried the macro definition. But it has too much side effects. Like `std::numeric_limits::max()` not being allowed anymore... – jpo38 Oct 10 '16 at 07:43
  • I'm not sure if it is a compiler bug. If min/max where one is a NaN has no defined result then it doesn't matter what gets returned. – CashCow Oct 10 '16 at 07:43
  • @GillBates: Unfortunately `:` are not permitted in macro names (at least for VS, it reports `error C2008: ':': unexpected in macro definition`) – jpo38 Oct 10 '16 at 07:55
4

If you get different results in debug and release, then the problem isn't getting different results. The problem is that one version, or probably both, are wrong. And that isn't fixed by disallowing std::min or std::max or replacing them with different functions that have defined results. You have to figure out which outcome you would actually want for each function call to get the correct result.

gnasher729
  • 51,477
  • 5
  • 75
  • 98
3

I'm not going to answer your question exactly, but instead of disallowing std::min and std::max altogether, you could educate your coworkers and make sure that you are consistently using a total order comparator instead of a raw operator< (implicitly used by many standard library algorithms) whenever you use a function that relies on a given order.

Such a comparator is proposed for standardization in P0100 — Comparison in C++ (as well as partial and weak order comparators), probably targeting C++20. Meanwhile, the C standard committee has been working for quite a while on TS 18661 — Floating-point extensions for C, part 1: Binary floating-point arithmic, apparently targeting the future C2x (should be ~C23), which updates the <math.h> header with many new functions required to implement the recent ISO/IEC/IEEE 60559:2011 standard. Among the new functions, there is totalorder (section 14.8), which compares floating point numbers according to the IEEE totalOrder:

totalOrder(x, y) imposes a total ordering on canonical members of the format of x and y:

  1. If x < y, totalOrder(x, y) is true.
  2. If x > y, totalOrder(x, y) is false.
  3. If x = y
    1. totalOrder(-0, +0) is true.
    2. totalOrder(+0, -0) is false.
    3. If x and y represent the same floating point datum:
      • If x and y have negative sign, totalOrder(x, y) is true if and only if the exponent of x ≥ the exponent of y.
      • Otherwise totalOrder(x, y) is true if and only if the exponent of x ≤ the exponent of y.
  4. If x and y are unordered numerically because x or y is NaN:
    1. totalOrder(−NaN, y) is true where −NaN represents a NaN with negative sign bit and y is a floating-point number.
    2. totalOrder(x, +NaN) is true where +NaN represents a NaN with positive sign bit and x is a floating-point number.
    3. If x and y are both NaNs, then totalOrder reflects a total ordering based on:
      • negative sign orders below positive sign
      • signaling orders below quiet for +NaN, reverse for −NaN
      • lesser payload, when regarded as an integer, orders below greater payload for +NaN, reverse for −NaN.

That's quite a wall of text, so here is a list that helps to see what's greater than what (from greater to lesser):

  • positive quiet NaNs (ordered by payload regarded as integer)
  • positive signaling NaNs (ordered by payload regarded as integer)
  • positive infinity
  • positive reals
  • positive zero
  • negative zero
  • negative reals
  • negative infinity
  • negative signaling NaNs (ordered by payload regarded as integer)
  • negative quiet NaNs (ordered by payload regarded as integer)

Unfortunately, this total order currently lacks library support, but it is probably possible to hack together a custom total order comparator for floating point numbers and use it whenever you know there will be floating point numbers to compare. Once you get your hands on such a total order comparator, you can safely use it everywhere it is needed instead of simply disallowing std::min and std::max.

Morwenn
  • 21,684
  • 12
  • 93
  • 152
3

If you compile using GCC or Clang, you can poison these identifiers.

#pragma GCC poison min max atoi /* etc ... */

Using them will issue a compiler error:

error: attempt to use poisoned "min"

The only problem with this in C++ is you can only poison "identifier tokens", not std::min and std::max, so actually also poisons all functions and local variables by the names min and max... maybe not quite what you want, but maybe not a problem if you choose Good Descriptive Variable Names™.

If a poisoned identifier appears as part of the expansion of a macro which was defined before the identifier was poisoned, it will not cause an error. This lets you poison an identifier without worrying about system headers defining macros that use it.

For example,

#define strrchr rindex
#pragma GCC poison rindex
strrchr(some_string, 'h');

will not produce an error.

Read the link for more info, of course.

https://gcc.gnu.org/onlinedocs/gcc-3.3/cpp/Pragmas.html

cat
  • 3,888
  • 5
  • 32
  • 61
1

You've deprecated std::min std::max. You can find instances by doing a search with grep. Or you can fiddle about with the headers themselves to break std::min, std::max. Or you can try defining min / max or std::min, std::max to the preprocessor. The latter is a bit dodgy because of C++ namespace, if you define std::max/min you don't pick up using namespace std, if you define min/max, you also pick up other uses of these identifiers.

Or if the project has a standard header like "mylibrary.lib" that everyone includes, break std::min / max in that.

The functions should return NaN when passed NaN, of course. But the natural way of writing them will trigger always false.

Malcolm McLean
  • 6,258
  • 1
  • 17
  • 18
-1

IMO the failure of the C++ language standard to require min(NaN, x) and min(x, NaN) to return NaN and similarly for max is a serious flaw in the C++ language standards, because it hides the fact that a NaN has been generated and results in surprising behaviour. Very few software developers do sufficient static analysis to ensure that NaNs can never be generated for all possible input values. So we declare our own templates for min and max, with specialisations for float and double to give correct behaviour with NaN arguments. This works for us, but might not work for those who use larger parts of the STL than we do. Our field is high integrity software, so we don't use much of the STL because dynamic memory allocation is usually banned after the startup phase.

dc42
  • 314
  • 3
  • 6