C++ 隐式转换

目标和动力

C/C++ 允许在类型之间以可能会修改底层值的方式转换值,例如由于上溢、下溢、符号变化、截断和精度损失。

这些转换可能是隐式发生的(在这种情况下,未指明作者希望发生转换)或者是显式发生;在这种情况下,它表明转化有意为之,并且作者了解后果。

默认情况下,Fucchsia C/C++ 代码会使用 -Wconversion 标志进行编译,该标志会禁止可能会更改值的隐式类型转换。当这种新的默认行为在 2020 年推出时,先前存在错误的源代码保持不变,关联的 build 定义也收到了抑制。我们希望清理这一旧版问题,因为忽略此警告可能会隐藏代码中的真实且非常细微的 bug。

技术背景

可能改变值的隐式类型转换示例:

float f = sqrt(2.0);
int   si = 0.5;
unsigned int ui = -1;

这些将会生成编译器错误,除非警告被抑制。

无法更改值的隐式类型转换示例:

double d = sqrtf(2.0f);
size_t size = 20;
int i = 4.0;

编译器会确定它们是安全的,并且绝不会发出有关隐式类型转换的警告。

在编译不显示隐式类型转换警告的代码时,BUILD.gn 定义可能如下所示:

source_set("foo") {
  ...
  # TODO(https://fxbug.dev/42136089): delete the below and fix compiler warnings
  configs += [ "//build/config:Wno-conversion" ]
}

//build/config/BUILD.gn 中的 "Wno-conversion" 配置目标的 visibility 列表下,您会看到源代码树中存在此抑制的所有实例的许可名单。

如何提供帮助

选择任务

选择提及 Wno-conversionbug 58162 的任何实例。(可选)浏览 //build/config/BUILD.gn 查看您所熟悉的任何代码的引用。

您可以忽略第三方代码(目录路径中的 third_party)。 Fuchsia 项目不拥有此代码,因此您无法对其进行修复。 不过,如果您愿意,也可以转到上游代码位置并在该位置贡献修复方法。也就是说,上游维护人员还必须同意启用 -Wconversion 标志,否则就会回归。

执行任务

取消 -Wno-conversion 抑制,重新构建并修复所有错误。

请注意,系统可能不会显示任何错误。通常,如果其他人更改了源代码,而这些源代码曾经存在隐式类型转换问题,但没有取消抑制,则通常就会发生这种情况。如果没有需要修复的问题,请继续将更改送审。否则,请继续阅读。

完全有可能不必要地抑制大量代码。如果您移除了一系列限制,并且您的更改通过了 CQ,那么没错,幸运的是。

示例:

简单的向下转换

[5838/95510] CC
obj/src/graphics/lib/compute/spinel/ext/geometry/geometry.svg_arc.c.o
../../src/graphics/lib/compute/spinel/ext/geometry/svg_arc.c:96:57: error:
implicit conversion loses floating-point precision: 'double' to 'float'
[-Werror,-Wimplicit-float-conversion]
  arc_params->phi = fmodf(x_axis_rotation_radians, M_PI * 2.0);
                      ~~~~~                          ~~~~~^~~~~

在这种情况下,您只需重写违规的代码行,使转换变得显式。

arc_params->phi = fmodf(x_axis_rotation_radians, (float)(M_PI * 2.0));

这没什么问题,因为此代码不会出现从双精度型精度到浮点型值的精度损失。其他更改可能会更复杂。例如,您可能需要在转换之前添加边界检查。

更改变量类型

在很多情况下,更改源变量类型比应用显式类型转换更合适。

../../garnet/bin/hwstress/memory_stress.cc:216:90: error: implicit conversion changes signedness: 'int' to 'uint64_t' (aka 'unsigned long') [-Werror,-Wsign-conversion]
                            MultiWordPattern(NegateWords((RotatePattern(every_sixth_bit, i))))));
                                                          ~~~~~~~~~~~~~                  ^

如果我们看一下更多代码:

std::vector<uint64_t> RotatePattern(std::vector<uint64_t> v, uint64_t n);
  ...
  for (int i = 0; i < 6; i++) {
    result.push_back(MakePatternWorkload(
        fxl::StringPrintf("Single bit clear 6-bit (%d/6)", i + 1),
        MultiWordPattern(NegateWords((RotatePattern(every_sixth_bit, i))))));
  }

我们可以看到,i 仅在此循环中使用。将其设为无符号类型会更简单:

  for (uint32_t i = 0; i < 6; i++) {
    result.push_back(MakePatternWorkload(
        fxl::StringPrintf("Single bit clear 6-bit (%u/6)", i + 1),
        MultiWordPattern(NegateWords((RotatePattern(every_sixth_bit, i))))));
  }

三元表达式中的多种类型转换

如果编译器对三元表达式中的操作数发出警告,那么从技术上讲,您可以向结果类型添加每个运算数的单个类型转换,但将整个表达式封装在类型转换中可能会更简洁。例如:

[5836/95510] CC obj/src/graphics/lib/compute/spinel/ext/geometry/geometry.arc.c.o
../../src/graphics/lib/compute/spinel/ext/geometry/arc.c:73:50: error: implicit conversion loses floating-point precision: 'double' to 'float' [-Werror,-Wimplicit-float-conversion]
  float const theta_sweep = theta_delta > 0.0f ? SPN_SWEEP_RADIANS : -SPN_SWEEP_RADIANS;
              ~~~~~~~~~~~                        ^~~~~~~~~~~~~~~~~
../../src/graphics/lib/compute/spinel/ext/geometry/arc.c:28:39: note: expanded from macro 'SPN_SWEEP_RADIANS'
#define SPN_SWEEP_RADIANS (2.0 * M_PI / 3.0)  // 120°
                           ~~~~~~~~~~~^~~~~
../../src/graphics/lib/compute/spinel/ext/geometry/arc.c:73:70: error: implicit conversion loses floating-point precision: 'double' to 'float' [-Werror,-Wimplicit-float-conversion]
  float const theta_sweep = theta_delta > 0.0f ? SPN_SWEEP_RADIANS : -SPN_SWEEP_RADIANS;
              ~~~~~~~~~~~                                            ^~~~~~~~~~~~~~~~~~

条件表达式的两个操作数都会出错。转换可以应用于表达式中的 SPN_SWEEP_RADIANS,或应用于整个条件表达式。

// Explicit casts at the error sites.
float const theta_sweep = theta_delta > 0.0f ? (float)(SPN_SWEEP_RADIANS) : (float)(-SPN_SWEEP_RADIANS);

// Different AST, but effectively cleaner.
float const theta_sweep = (float)(theta_delta > 0.0f ? SPN_SWEEP_RADIANS : -SPN_SWEEP_RADIANS);

从 AST 的角度来看,表达式有所不同,但逻辑和优化 Codegen 仍然相同。

尽量不要在宏中应用类型转换,而是在它们的调用中应用类型转换

宏(尤其是头文件中的宏)往往会在多个位置使用,并且接受不同类型的参数。通常,最好对宏参数或宏本身应用类型转换,而不是在宏内部应用类型。例如:

../../src/virtualization/third_party/fdt/fdt.c:143:16: error: implicit conversion changes signedness: 'unsigned long' to 'int' [-Werror,-Wsign-conversion]
        *nextoffset = FDT_TAGALIGN(offset);
                    ~ ^~~~~~~~~~~~~~~~~~~~
../../src/virtualization/third_party/fdt/libfdt_internal.h:56:26: note: expanded from macro 'FDT_TAGALIGN'
#define FDT_TAGALIGN(x) (FDT_ALIGN((x), FDT_TAGSIZE))
                         ^~~~~~~~~~~~~~~~~~~~~~~~~~~
../../src/virtualization/third_party/fdt/libfdt_internal.h:55:40: note: expanded from macro 'FDT_ALIGN'
#define FDT_ALIGN(x, a) (((x) + (a)-1) & ~((a)-1))
                         ~~~~~~~~~~~~~~^~~~~~~~~~
../../src/virtualization/third_party/fdt/fdt.c:143:29: error: implicit conversion changes signedness: 'int' to 'unsigned long' [-Werror,-Wsign-conversion]
        *nextoffset = FDT_TAGALIGN(offset);
                      ~~~~~~~~~~~~~^~~~~~~
../../src/virtualization/third_party/fdt/libfdt_internal.h:56:37: note: expanded from macro 'FDT_TAGALIGN'
#define FDT_TAGALIGN(x) (FDT_ALIGN((x), FDT_TAGSIZE))
                         ~~~~~~~~~~~^~~~~~~~~~~~~~~~
../../src/virtualization/third_party/fdt/libfdt_internal.h:55:28: note: expanded from macro 'FDT_ALIGN'
#define FDT_ALIGN(x, a) (((x) + (a)-1) & ~((a)-1))
                           ^  ~

这两个错误均指向最内层的宏展开。与其对第一个错误中的 & 或第二个错误中的 x 执行类型转换,不如对宏参数偏移量和原始语句中 FDT_TAGALIGN 的结果应用转换。

        *nextoffset = (int)(FDT_TAGALIGN((unsigned)offset));

使用模板化类型

与宏类似,在模板化函数中应用类型转换时,请确保使用模板化类型,而不是警告中指定的类型。其他代码可能会将此模板用于不同的类型。例如,在:

template <typename T>
constexpr typename std::make_signed<T>::type ConditionalNegate(
    T x,
    bool is_negative) {
  static_assert(std::is_integral<T>::value, "Type must be integral");
  using SignedT = typename std::make_signed<T>::type;
  using UnsignedT = typename std::make_unsigned<T>::type;
  return static_cast<SignedT>(
      (static_cast<UnsignedT>(x) ^ -SignedT(is_negative)) + is_negative);
}

我们会获得:

../../zircon/third_party/ulib/safemath/include/safemath/safe_conversions_impl.h:75:36: error: implicit conversion changes signedness: 'SignedT' (aka 'long') to 'unsigned long' [-Werror,-Wsign-conversion]
      (static_cast<UnsignedT>(x) ^ -SignedT(is_negative)) + is_negative);
                                 ~ ^~~~~~~~~~~~~~~~~~~~~

我们应 static_cast 为 UnsignedT 而不是无符号长整型。

  return static_cast<SignedT>(
      (static_cast<UnsignedT>(x) ^ static_cast<UnsignedT>(-SignedT(is_negative))) + is_negative);

回调函数

这可能是一种特别具有欺骗性的错误,因为调用堆栈可能没有必要指向确切的问题。例如,为回调函数分配的参数类型不匹配。例如,如果我们查看:

../../sdk/lib/fit/include/lib/fit/function_internal.h:70:19: error: implicit conversion changes signedness: 'long' to 'uint64_t' (aka 'unsigned long') [-Werror,-Wsign-conversion]
    return target(std::forward<Args>(args)...);
           ~~~~~~ ^~~~~~~~~~~~~~~~~~~~~~~~
../../sdk/lib/fit/include/lib/fit/function_internal.h:91:60: note: in instantiation of member function 'fit::internal::target<(lambda at ../../examples/fidl/test/launcher.cc:67:7), true, false, void, long, fuchsia::sys::TerminationReason>::invoke' requested here
    &unshared_target_type_id, &inline_target_get, &target::invoke, &target::move, &target::destroy};
                                                           ^
../../sdk/lib/fit/include/lib/fit/function_internal.h:370:45: note: in instantiation of static data member 'fit::internal::target<(lambda at ../../examples/fidl/test/launcher.cc:67:7), true, false, void, long, fuchsia::sys::TerminationReason>::ops' requested here
      ops_ = &target_type<DecayedCallable>::ops;
                                            ^
../../sdk/lib/fit/include/lib/fit/function_internal.h:297:5: note: in instantiation of function template specialization 'fit::internal::function_base<16, false, void (long, fuchsia::sys::TerminationReason)>::initialize_target<(lambda at ../../examples/fidl/test/launcher.cc:67:7)>' requested here
    initialize_target(std::forward<Callable>(target));
    ^
../../sdk/lib/fit/include/lib/fit/function.h:253:11: note: in instantiation of function template specialization 'fit::internal::function_base<16, false, void (long, fuchsia::sys::TerminationReason)>::assign<(lambda at ../../examples/fidl/test/launcher.cc:67:7), void>' requested here
    base::assign(std::forward<Callable>(target));
          ^
../../examples/fidl/test/launcher.cc:66:43: note: in instantiation of function template specialization 'fit::function_impl<16, false, void (long, fuchsia::sys::TerminationReason)>::operator=<(lambda at ../../examples/fidl/test/launcher.cc:67:7)>' requested here
  client_controller.events().OnTerminated =
                                          ^

您可能想查看 sdk/lib/fit/include/lib/fit/function_internal.h:70 中的代码,但根本问题位于 examples/fidl/test/launcher.cc:67 中

  client_controller.events().OnTerminated =
      [&loop, &client_status](uint64_t code, fuchsia::sys::TerminationReason reason) {
...

如果我们查看 sdk/fidl/fuchsia.sys/component_controller.fidl,就会发现 OnTerminated 接受 int64 作为第一个参数,但我们分配一个接受 uint64_t 作为第一个参数的 lambda。此处的解决方法是改为接受 int64_t 作为 lambda 参数。

  client_controller.events().OnTerminated =
      [&loop, &client_status](int64_t code, fuchsia::sys::TerminationReason reason) {
...

无损向下

如果您希望对某个变量进行向下转换,但又不想容忍转换期间出现的损失, safeMath 库提供了一组实用函数(包括 checked_cast),这些函数会在向下转换期间发生数据丢失时进行断言。

void fun(uint64_t block_number) {
  // Downcast block_number because underlying layer expects uint32_t.
  uint32_t block = safemath::checked_cast<uint32_t>(block_number);
  ....
}

上面的 checked_cast 断言 block_number 是否大于 std::numeric_limits<uint32_t>::max()

若要使用 safeMath,请在 BUILD.gn 文件中添加 //zircon/third_party/ulib/safemath 作为依赖项。如需查看 BUILD.gn 示例,请点击此处;如需查看用法示例,请点击此处

完成任务

请按以下说明在更改说明中标记封面 bug:

Bug: 58162

"Wno-conversion" 配置目标的 visibility 列表下,移除对您在 //build/config/BUILD.gn 中清理的构建目标的任何引用。

通过所有者查找审核者并合并更改。

示例

赞助商

如有疑问或想了解最新状态,请与我们联系: