【问题标题】:Uninitialised variable warning for variable that is constructed构造变量的未初始化变量警告
【发布时间】:2021-12-06 07:35:58
【问题描述】:

因此,我最近开始了一场运动,以清除在 GCC (-Wall -Wextra -Wpedantic) 中启用所有编译器警告时收到的一些警告,但我收到了一个关于未初始化值的相当混乱的警告。这是正在发生的事情的一个(稍微简化的)视图:

#include <cstdint>
#include <iostream>

constexpr uint64_t MASK = 0xFFFFFFFFFFFFFFFF;

double do_something(const double& rhs) {
   double tmp(rhs);
   reinterpret_cast<uint64_t&>(tmp) &= MASK;
   return tmp;
} 

int main() {
    std::cout << do_something(3.14159);
}

编译

g++ -std=c++17 -O3 -Wall -Wextra -Wpedantic

基本上我将位掩码MASK 应用于双精度值,然后对原始数据和掩码数据执行一些其他操作。另一个操作涉及与此处无关的额外状态,并且位掩码存储在其他位置但已正确定义。奇怪的是,我得到了以下编译器警告

<source>: In function 'double do_something(const double&)':
<source>:8:32: warning: dereferencing type-punned pointer will break strict-aliasing rules [-Wstrict-aliasing]
    8 |    reinterpret_cast<uint64_t&>(tmp) &= MASK;
      |                                ^~~
<source>:8:37: warning: 'tmp' is used uninitialized [-Wuninitialized]
    8 |    reinterpret_cast<uint64_t&>(tmp) &= MASK;
      |    ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~^~~~~~~
<source>:7:11: note: 'tmp' declared here
    7 |    double tmp(rhs);

尽管警告的上下文指向重新解释数据的 &= 操作。我认为这是因为编译器无法通过 reinterpret cast 看到在 (reinterpreted) 对 uint64_t 的引用中保存的上述行上初始化的有效数据。它是否正确?如果没有,谁能启发我?

免责声明:我知道你们中的一些人会因重新解释演员而被推迟,但这对于这段特定代码的运行方式至关重要,并且(除了警告之外)它在这种情况下是一个“安全”的操作。

【问题讨论】:

  • 无法复制,请发minimal reproducible example
  • reinterpret_cast&lt;uint64_t&amp;&gt;(tmp) &amp;= MASK; 看起来像 UB。您正在读取和写入变量,就好像它不是一样的东西。您使用的是什么编译器和选项?也许 UB 让它做了一些你没想到的事情,导致 tmp 未初始化?
  • memcpy 是要走的路(或自 C++20 起为 std::bit_cast)。否则你会违反严格的别名规则。
  • 感谢您添加示例。此代码来自一个成熟的库,该库已在所有平台和所有主要编译器(GCC、Clang、MSVC、Intel)上进行了详尽的测试。转换为与原始值大小相同的整数类型不应该是未定义的行为,尽管像整数一样对其进行操作肯定不是有效的双精度操作,但这不是重点。
  • @SamM "...转换为与原始值大小相同的整数类型不应是未定义的行为..." - 它由 C++ 标准定义为未定义行为阅读此处的类型别名部分 - en.cppreference.com/w/cpp/language/reinterpret_cast

标签: c++ gcc gcc-warning


【解决方案1】:

使用memcpy。不要键入punne 指针。

#include <cstring>
#include <cstdint>
#include <iostream>

constexpr uint64_t MASK = 0xFFFFFFFFFFFFFFFF;

double do_something2(const double& rhs) {
   double tmp(rhs);
   uint64_t tmp64 = 0;
   static_assert(sizeof(uint64_t) >= sizeof(double));
   memcpy(&tmp64, &tmp, sizeof(double));
   tmp64 &= MASK;
   memcpy(&tmp, &tmp64, sizeof(double));
   return tmp;
}

int main() {
    std::cout << do_something2(3.14159);
}

您也可以使用unsigned char 来检查任何内容:

double do_something3(const double& rhs) {
    double tmp(rhs);
    unsigned char *tmpu = reinterpret_cast<unsigned char*>(&tmp);
    uint64_t mask = MASK; // TODO: handle endianess
    for(int i = 0; i < sizeof(double) / CHAR_BIT; ++i) {
        const unsigned char tmpmask = mask;
        mask >>= CHAR_BIT;
        *tmpu &= tmpmask;
    }
    return tmp;
}

【讨论】:

  • 在实际代码中,我生成了 MASK 和整数类型来匹配 double 的大小和字节序。我不明白如何转换为 unsigned char 指针然后迭代字节与在转换为 uint64_t 后简单地使用就地掩码操作有很大不同。
  • @SamM 因为转换为uint64_t 会调用未定义的行为。您只能转换为原始(或相关)类型,doubleuint64_t 之间没有关系。
  • @SamM I don't understand how casting into unsigned char pointers and then iterating over bytes 请研究严格的别名规则,就在警告信息中。我使用 unsigned char 类型访问对象 - 根据eel.is/c++draft/expr#basic.lval-11 这很好。 s substantially different from 规则很明确,使用uint64_t 访问double 是未定义的行为。
  • @SamM 我看到了你的第一条评论。我之前的评论是给 KamilCuk 的。当他更新答案时,我删除了该评论。关于您的第一条评论和后来的 cmets:由于标准中明确写了 UB 并且您“知道这一点”,您真正想知道的是什么?这就是“如何”部分。您是否想知道为什么他们选择将其设为 UB?
  • 实际上你在上面的一个 cmets 中回答了我的问题:“既然操作 reinterpret_cast(tmp) &= MASK; 有 UB,为什么编译器还要初始化 tmp?它可以跳过那。”这就引出了一个问题,这个操作是否可能没有执行我期望的操作。 (这不是我迄今为止所经历的,但仍然如此。)
猜你喜欢
  • 1970-01-01
  • 1970-01-01
  • 2017-11-24
  • 1970-01-01
  • 1970-01-01
  • 2021-07-24
  • 1970-01-01
  • 1970-01-01
  • 2016-07-07
相关资源
最近更新 更多