有没有一种方法可以防止开发人员使用std::min、std::max函数?

12
我们有一个算法库,对可能为NaN的数字进行了大量的std::min/std::max操作。考虑到这篇帖子:为什么Release/Debug在std::min上有不同的结果?,我们意识到这是明显不安全的。
是否有一种方法可以防止开发人员使用std::min/std::max
我们的代码同时编译于VS2015和g++。我们有一个共同的头文件被所有源文件包含(通过VS2015的/FI选项和g++的-include)。是否有任何片段代码/编译指示可以放置在此处,使使用std::minstd::max的cpp文件无法进行编译?
顺便说一句,像STL头文件那样使用此函数的遗留代码不应受到影响。只有我们编写的代码应受影响。

即使它是 UB(因为函数在 std 中),你也可以将该函数标记为 已弃用 - Jarod42
21
我猜你的方法是可以理解但不是最佳的:在算术运算中使用NaN,即除了标志/结果之外还有更多用途,在你的设计中是一个错误。我会在算法中进行检查,使用自己的NaN,尝试避免使用它或重新设计库。但是防止开发人员使用标准库工具注定要失败,而且在我看来这是徒劳无功。 - Superlokkus
@Jarod42:你是怎么做到的? - jpo38
最小值/最大值是否应该定义为NaN?无论如何,如果a或b是NaN,则(a<b) ? a : b将返回b,因为a<b为false,同样的,(a>b) ? a: b将返回b,但(a<b) ? b : a对于max将返回a,因此,如果其中一个是NaN,则最大值的合理实现是未定义的。 - CashCow
@jpo38:像这样 - Jarod42
显示剩余7条评论
9个回答

32

我不认为禁用标准库函数是正确的方法。首先,NaN 是浮点数值工作方式的基本方面。你需要禁用所有其他类型的东西,比如 sort()lower_bound() 等等。此外,程序员因为创造力而受到报酬,我怀疑任何使用 std::max() 的程序员如果 std::max(a, b) 不起作用,都不会犹豫地使用 a < b? b: a

另外,显然你不想禁用那些没有 NaN 的类型(如整数或字符串)的 std::max()std::min() 。所以,你需要一种比较受控制的方法。

std 命名空间中没有可移植的方法来禁用任何标准库算法。你可以通过提供适当的 delete 重载来防止使用这些算法,例如:

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
}

3
这在很大程度上依赖于未定义的行为。 - Hatted Rooster
8
根据17.6.4.2.1 [namespace.std]第1段的规定:“如果未经指定,C++程序在向命名空间std或std命名空间中的命名空间添加声明或定义时的行为未定义...” 没有允许重载的规范。基本原因是标准库可能使用这些函数,使用户代码使用不同的定义违反了一个定义规则(One Definition rule)。 - Dietmar Kühl
1
@DietmarKühl:投诉源于Debug和Release具有不同的行为。键入其内联条件形式的程序员将不会遇到这个特定的问题。 - Joshua
2
@Joshua 在另一个问题中的问题是编译器决定切换比较...我不明白为什么即使使用内联条件,编译器也不能这样做...或者有什么阻止编译器将 x < y 改为 y >= x(对于浮点数)的东西吗? - Bakuriu
@Bakuriu:不要在调试和发布之间更改优化标志!我已经被烧过很多次了。 - Joshua
显示剩余3条评论

18

我要发一些哲学性的言论,而不是只是代码。但是我认为最好的方法是教育那些开发者,并解释为什么他们不应该以特定方式编码。如果您能给他们一个好的解释,那么他们不仅会停止使用您不想让他们使用的函数,而且还能向团队中的其他开发人员传递信息。

我相信强制他们只会让他们想出变通方法。


3
这个。再加上一个好的工作流程,例如包括拉请求时的代码审查(例如在Github / Bitbucket / ...上),这应该可以解决大部分问题。 - Synxis

7

由于不允许修改std,以下是未定义行为,但在您的情况下可能有效。 将函数标记为已弃用:

自c++14以来,使用deprecated属性:

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&));
}

演示

而在之前

#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


4

除了一些特殊情况外,由于不允许更改std,因此没有便携式方法来执行此操作。

然而,一个解决方案是在包含任何您的代码之前进行以下操作:

#define max foo

然后,std::maxmax都会发出编译时失败的消息。

但是,如果我是您,我将习惯于std::maxstd::min在您的平台上的行为。 如果它们没有按照标准应该做的那样工作,请向编译器供应商提交错误报告。


尝试了宏定义,但是它有太多的副作用。比如 std::numeric_limits<T>::max() 不再被允许使用... - jpo38
我不确定这是否是编译器的错误。如果min/max其中一个是NaN,没有定义的结果,那么返回什么并不重要。 - CashCow
@GillBates:不幸的是,:在宏名称中是不允许的(至少对于VS而言,它会报告“error C2008:‘:’:在宏定义中意外出现”)。 - jpo38

4

如果你在调试和发布版本中得到不同的结果,那么问题不在于结果不同。问题在于一个版本或者可能两个版本都是错误的。禁止使用std::min或std::max或用具有确定结果的不同函数替换它们并不能解决这个问题。你必须找出每个函数调用实际上需要哪种结果才能得到正确的结果。


3

我不会直接回答你的问题,但是与其完全禁止使用std::minstd::max,你可以向你的同事们提供教育,并确保每当使用依赖于给定顺序的函数时,使用总排序比较器而不是原始的operator<(许多标准库算法隐式使用)。

这样的比较器在P0100 — Comparison in C++中被建议作为标准化内容之一(以及部分和弱排序比较器),可能针对的是C++20。与此同时,C��准委员会已经在TS 18661 — Floating-point extensions for C, part 1: Binary floating-point arithmic上工作了相当长的时间,显然是针对未来的C2x(应该是~C23),它将使用许多新函数更新<math.h>头文件,以实现最近的ISO/IEC/IEEE 60559:2011标准所需。其中,有一个新的函数是totalorder(第14.8节),它根据IEEEtotalOrder比较浮点数:

totalOrder(x, y)对xy的规范成员施加完全排序:
  1. 如果xy,则totalOrder(x, y)为真。
  2. 如果xy,则totalOrder(x, y)为假。
  3. 如果x = y
    1. totalOrder(-0, +0) 为真。
    2. totalOrder(+0, -0) 为假。
    3. 如果xy表示相同的浮点数据:
      • 如果xy具有负号,则当且仅当x的指数≥y的指数时,totalOrder(x, y)为真。
      • 否则,当且仅当x的指数≤y的指数时,totalOrder(x, y)为真。
  4. 如果由于xy是NaN而在数值上无序排列:
    1. totalOrder(−NaN, y)为真,其中- NaN表示带有负号位的NaN,y是浮点数。
    2. totalOrder(x, +NaN)为真,其中+NaN表示带有正号位的NaN,x是浮点数。
    3. 如果xy都是NaN,则totalOrder基于完全排序反映如下:
      • 负符号顺序低于正符号
      • 对于+NaN,信令次于安静,对于- NaN相反
      • 较小有效载荷作为整数时,以下订单低于较大有效载荷对于+NaN,对于- NaN相反。
这里是一个列表,可以看出哪些比哪些更大(从大到小):
  • 正无穷大
  • 正实数
  • 正零
  • 负零
  • 负实数
  • 负无穷大

很不幸,当前这个完全顺序(total order)缺乏库支持,但是可能可以自己编写一个针对浮点数的定制化完全顺序比较器,并在需要比较浮点数时使用它。一旦您获得了这样的完全顺序比较器,您就可以安全地在需要使用它的任何地方使用它,而不仅仅是禁止使用 std::minstd::max


3
如果您使用GCC或Clang编译,您可以“毒化”这些标识符。
#pragma GCC poison min max atoi /* etc ... */

使用它们会引发编译错误:

error: attempt to use poisoned "min"

在C ++中,这种方法的唯一问题是你只能污染“标识符令牌”,而不是 std :: min 和 std :: max ,所以实际上也污染了所有名称为 min 和 max 的函数和局部变量...也许这不是你想要的,但如果你选择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.

点击链接获取更多信息。

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


1
您已将std::min std::max弃用。 您可以使用grep搜索实例。或者您可以调整头文件本身以破坏std::min,std::max。或者您可以尝试将min / max或std::min,std::max定义为预处理器。 后者有点危险,因为涉及C++命名空间,如果您定义了std::max / min,则不会使用命名空间std,如果您定义了min / max,则还会使用这些标识符的其他用途。

或者,如果项目具有像“mylibrary.lib”这样的标准头文件,则在其中破坏std::min / max。

当传递NaN时,函数应返回NaN。 但是,编写它们的自然方式将始终触发false。


-1

依我看,C++语言标准未要求min(NaN, x)和min(x, NaN)返回NaN,max同理,这是C++语言标准的一个严重缺陷,因为它隐藏了NaN已被生成的事实,并导致出乎意料的行为。很少有软件开发人员进行足够的静态分析以确保NaN永远不会为所有可能的输入值而生成。因此,我们声明自己的min和max模板,并为float和double特化,以处理带有NaN参数的正确行为。这对我们有效,但对于使用比我们更大部分STL的人可能无效。我们的领域是高完整性软件,因此在启动阶段后通常禁止使用动态内存分配,我们不使用STL的大部分功能。


网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接