上下文
我正在写一个函数,它计算定时器应用程序的某个指数值。它只需要2^x,直到某个阈值maxVal,在这种情况下,应该返回阈值。而且,在所有情况下,边缘情况应该被接受。
util.cpp:
# include "util.h"
# include <iostream>
int calculateExponentialBackoffDelay(int x, int maxVal)
{
int y;
if(x <= 0 || maxVal <= 0) return maxVal;
else if (x > maxVal) return maxVal;
y = std::pow(2, x);
std::cout << "y = " << y << std::endl;
if(y > maxVal) return maxVal;
else if(y < 0) return maxVal;
else return y;
}
现在,我使用GoogleTest依赖项提取来创建一个CMake配置。
CMakeLists.txt:
cmake_minimum_required(VERSION 3.14)
project(my_project)
# GoogleTest requires at least C++14
set(CMAKE_CXX_STANDARD 14)
set(CMAKE_BUILD_TYPE Release)
include(FetchContent)
FetchContent_Declare(
googletest
GIT_REPOSITORY https://github.com/google/googletest.git
GIT_TAG release-1.12.1
)
# For Windows: Prevent overriding the parent project's compiler/linker settings
set(gtest_force_shared_crt ON CACHE BOOL "" FORCE)
FetchContent_MakeAvailable(googletest)
enable_testing()
include_directories(
${CMAKE_CURRENT_LIST_DIR}/
)
# Add the main source compilation units
add_executable(
test_calc
test_calc.cpp
util.cpp
)
target_link_libraries(
test_calc
GTest::gtest_main
)
include(GoogleTest)
gtest_discover_tests(test_calc)
我在我写的函数上运行了一个测试,测试了一些边界条件,其中之一是测试如果2^x〉maxVal,那么maxVal应该被返回(因为2^x的结果高于最大值),这就是阈值。
测试_计算.cpp:
# include "util.h"
# include <climits>
# include <gtest/gtest.h>
TEST(util_tests, ExponentialBackoff)
{
int x, maxVal, res;
// Test x = maxVal and maxVal = 1000
// Expected output: 1000
maxVal = 1000;
x = maxVal;
EXPECT_EQ(maxVal, calculateExponentialBackoffDelay(x, maxVal));
}
当我将x和maxVal设为1000时,计算出的结果是2^(1000),因为它是一个很大的数字,所以会溢出/回绕到一个真正的负数(-2 147
483 ′ 648),这是意料之中的,因此我的测试会期望x=1000,maxVal=1000的结果〈0。
问题
这就是事情出乎意料的发展。我在我的build目录中运行ctest,所有的测试用例都通过了。然后我在CMakeLists.txt中将一行从...:
set(CMAKE_BUILD_TYPE Debug)
...到...
set(CMAKE_BUILD_TYPE Release)
...并且该测试用例失败:
1: Expected equality of these values:
1: maxVal
1: Which is: 1000
1: calculateExponentialBackoffDelay(x, maxVal)
1: Which is: -2147483648
因此,由于某种原因,函数体中y〈0的情况没有达到,相反,我们返回了环绕结果。
为什么会这样?我做错了什么?我试着用Linux strip -s test_calc
检查它是否是一个符号,同时在CMake中保留调试配置,结果发现测试用例仍然通过。CMake还做了什么来改变结果二进制文件的比较行为?
2条答案
按热度按时间svmlkihl1#
浮点-积分转换
行为未定义。对
int y = std::pow(2, x)
与x > 31
的结果的任何预期都被视为无效,并可能导致函数calculateExponentialBackoffDelay
的任何结果。在这种特殊情况下,编译器知道
y = std::pow(2, x)
对于x
的有效值总是大于0,并丢弃分支if (y < 0)
。emeijp432#
当改变构建类型改变结果时,你首先想到的应该是“代码中某处的未定义行为”。事实确实如此。将
double
(pow
的结果)转换为整数类型,其值超出该整数类型的范围,这就是未定义行为。引用cppreference的话:
任何真实的浮点类型的有限值都可以隐式转换为任何整数类型。除了上面的布尔转换所涉及的情况外,规则如下:
2的1000次幂大约是1e301,这远远超出了
int
类型的任何可能范围,因此将其转换为y = std::pow(2, x);
绝对是一个UB。下面是一个尝试来解释它:
1.编译器可以基于代码中没有UB的假设来优化代码。
1.除非有UB,
y = std::pow(2, x);
的唯一有效结果是正整数(因为我们得到的是正整数的指数,它只能是0或正数)1.在程序中可以没有UB(点1),因此条件
if (y < 0)
总是false
(点2),并且可以被优化掉。但这只是我的猜测,它可能是正确的,也可能是完全错误的。编译器被允许对包含任何UB的代码做任何事情。