constexpr 和使用重新解释强制转换的静态 const void 指针的初始化,哪个编译器是正确的?

Posted

技术标签:

【中文标题】constexpr 和使用重新解释强制转换的静态 const void 指针的初始化,哪个编译器是正确的?【英文标题】:constexpr and initialization of a static const void pointer with reinterpret cast, which compiler is right? 【发布时间】:2014-06-24 23:48:03 【问题描述】:

考虑以下代码:

struct foo 
  static constexpr const void* ptr = reinterpret_cast<const void*>(0x1);
;

auto main() -> int 
  return 0;

上述示例在 g++ v4.9 (Live Demo) 中编译良好,但在 clang v3.4 (Live Demo) 中编译失败并生成以下错误:

错误:constexpr 变量 'ptr' 必须由常量表达式初始化

问题:

根据标准,这两个编译器中哪个是正确的?

声明此类表达式的正确方法是什么?

【问题讨论】:

auto main() -&gt; int 的人怎么了? @KerrekSB “也许 OP 是 XML 的发明者?”你让我…… 我很好奇这个用例是什么? @ShafikYaghmour 它可以用作另一个特殊的“nullptr”。所以一个指针可能有三种状态,有效指针,nullptr,特殊状态值。 另一个用例是初始化指向微控制器外围设备的指针常量。这些地址通常在设备特定的头文件中以整数宏的形式给出。 【参考方案1】:

TL;DR

clang 是正确的,这是已知的gcc 错误。您可以改用 intptr_t 并在需要使用该值时进行转换,或者如果这不可行,则 gccclang 都支持一些记录在案的解决方法,应该允许您的特定用例。

详情

如果我们转到draft C++11 standard 部分5.19 常量表达式 段落2 说,clang 在这一点上是正确的:

条件表达式是核心常量表达式,除非它 涉及以下之一作为潜在评估的子表达式 [...]

并包括以下项目符号:

——重新解释转换 (5.2.10);

一个简单的解决方案是使用intptr_t:

static constexpr intptr_t ptr = 0x1;

然后在需要时再施放:

reinterpret_cast<void*>(foo::ptr) ;

这可能很诱人,但这个故事变得更有趣了。这是已知的并且仍然打开gcc 错误请参阅Bug 49171: [C++0x][constexpr] Constant expressions support reinterpret_cast。从讨论中可以清楚地看出,gcc 开发人员对此有一些明确的用例:

我相信我发现了 reinterpret_cast 的一致用法 C++03 中可用的表达式:

//---------------- struct X   X* operator&(); ;

X x[2];

const bool p = (reinterpret_cast<X*>(&reinterpret_cast<char&>(x[1]))
- reinterpret_cast<X*>(&reinterpret_cast<char&>(x[0]))) == sizeof(X);

enum E  e = p ; // e should have a value equal to 1
//----------------

这个程序基本上演示了 C++11 库的技术 函数 addressof 基于并因此排除 reinterpret_cast unconditionally 来自核心语言中的常量表达式会使这个有用的程序无效,并且不可能 将 addressof 声明为 constexpr 函数。

但无法为这些用例生成异常,请参阅closed issues 1384:

虽然在地址常量中允许 reinterpret_cast C++03 中的表达式,此限制已在某些 编译器,并没有证明会破坏大量代码。工作组 认为处理具有 tpes 的指针的复杂性 更改(指针算术和取消引用不能被允许 这样的指针)超过了放松当前的可能效用 限制。

BUT 显然 gccclang 支持一个小文档扩展,允许使用 __builtin_constant_p (exp) 对非常量表达式进行常量折叠,因此以下表达式被 gccclang:

static constexpr const void* ptr = 
  __builtin_constant_p( reinterpret_cast<const void*>(0x1) ) ? 
    reinterpret_cast<const void*>(0x1) : reinterpret_cast<const void*>(0x1)  ;

找到这方面的文档几乎是不可能的,但是这个带有以下 sn-ps 的 llvm commit is informative 提供了一些有趣的阅读:

支持 gcc __builtin_constant_p() 吗? ... : ... C++11 中的折叠技巧

和:

// __builtin_constant_p ? : is magical, and is always a potential constant.

和:

// This macro forces its argument to be constant-folded, even if it's not
// otherwise a constant expression.
#define fold(x) (__builtin_constant_p(x) ? (x) : (x))

我们可以在 gcc-patches 电子邮件中找到对此功能更正式的解释:C constant expressions, VLAs etc. fixes,其中写道:

此外,__builtin_constant_p 调用的规则是有条件的 实现中的表达式条件比那些更宽松 在正式模型中:被选中的一半条件表达式 完全折叠而不考虑它是否正式为常数 表达式,因为 __builtin_constant_p 测试完全折叠的参数 自己。

【讨论】:

不能是constexpr 的任意例外列表开始让我烦恼... @TemplateRex 所以我在上一个 llvm 社交和 IIUC 中与某人聊天,编译器必须跟踪大量 TBAA 数据才能在 reinterpret_cast 的 constexpr 中检测 UB,这将非常昂贵。我必须跟进,看看我是否做对了。 我同意跟踪 UB 是有问题的。 OTOH,goto 和结构化绑定之类的东西应该可以为 constepxr 做。 有一个 constexpr goto 提案,但被拒绝了,取而代之的是更全面的方法,See Botond's trip report in the Rejected section。 正如 Kishore 上面提到的,gcc 不再接受这个技巧(6.4 仍然有效,7.1 中断,8.0 更改错误消息)。 Clang 9.0(当前最新版本)仍然接受它。无赖。【参考方案2】:

Clang 是对的。重新解释转换的结果绝不是常量表达式(参见 C++11 5.19/2)。

常量表达式的目的是它们可以作为值进行推理,并且值必须是有效的。您正在编写的内容不能证明是一个有效的指针(因为它不是对象的地址,或者通过指针算术与对象的地址相关),因此您不允许将其用作常量表达式。如果您只想存储号码1,请将其存储为uintptr_t,并在使用站点进行重新解释。


顺便说一句,为了详细说明“有效指针”的概念,请考虑以下constexpr 指针:

constexpr int const a[10] =  1 ;
constexpr int * p1 = a + 5;

constexpr int const b[10] =  2 ;
constexpr int const * p2 = b + 10;

// constexpr int const * p3 = b + 11;    // Error, not a constant expression

static_assert(*p1 == 0, "");             // OK

// static_assert(p1[5] == 0, "");        // Error, not a constant expression

static_assert(p2[-2] == 0, "");          // OK

// static_assert(p2[1] == 0, "");        // Error, "p2[1]" would have UB

static_assert(p2 != nullptr, "");        // OK

// static_assert(p2 + 1 != nullptr, ""); // Error, "p2 + 1" would have UB

p1p2 都是常量表达式。但是指针运算的结果是不是常量表达式,就看是不是UB了!如果允许 reinterpret_casts 的值是常量表达式,这种推理基本上是不可能的。

【讨论】:

你怎么知道这不是一个有效的指针?在我的,它指向地址 1,这很好。 (微控制器) @Deduplicator:如果您真的需要引用,而不仅仅是引用,那么标准的引用条款会说“A conditional-expression is a core constant表达式 除非它涉及以下之一”,其中以下列表包括“a reinterpret_cast @HolyBlackCat:确实,0 是一个空指针常量。但这与将任意整数值重新解释为指针无关。 @Deduplicator:常量表达式核心常量表达式的子集,如以下第 5.19/3 段所述。 您回答中的第一段无疑是有效的。第二段,不多说了。重复数据删除器是绝对正确的。在0x1 是内存中字节地址的系统上,它是一个有效的指针,尽管它不是一个常量表达式。【参考方案3】:

我在为 AVR 微控制器编程时也遇到了这个问题。 Avr-libc 有头文件(包含在&lt;avr/io.h&gt; 中,通过定义macros such as 为每个微控制器提供寄存器布局:

#define TCNT1 (*(volatile uint16_t *)(0x84))

这允许像使用普通变量一样使用TCNT1,并且任何读取和写入都会自动定向到内存地址0x84。但是,它还包含一个(隐式)reinterpret_cast,它可以防止在常量表达式中使用这个“变量”的地址。而且由于这个宏是由 avr-libc 定义的,因此更改它以删除强制转换并不是一个真正的选择(并且自己重新定义这些宏是可行的,但随后需要为所有不同的 AVR 芯片定义它们,复制来自 avr-libc 的信息) .

由于 Shafik 建议的折叠技巧在 gcc 7 及更高版本中似乎不再适用,我一直在寻找另一种解决方案。

更仔细地查看 avr-libc 头文件,turns out they have two modes: - 通常,它们会定义类似变量的宏,如上所示。 - 在汇编器内部使用时(或包含在 _SFR_ASM_COMPAT 中定义时),它们定义只包含地址的宏,例如: #define TCNT1 (0x84)

乍一看后者似乎很有用,因为您可以在包含&lt;avr/io.h&gt; 之前设置_SFR_ASM_COMPAT 并简单地使用intptr_t 常量并直接使用地址,而不是通过指针。但是,由于您只能包含一次 avr-libc 标头(iow,只有 TCNT1 作为变量类宏或地址),因此此技巧仅适用于不包含任何其他文件的源文件那将需要类似变量的宏。在实践中,这似乎不太可能(尽管也许您可以在 .h 文件中声明 constexpr(类?)变量并在 .cpp 文件中分配一个不包含其他内容的值?)。

无论如何,我找到了another trick by Krister Walfridsson,它将这些寄存器定义为 C++ 头文件中的外部变量,然后使用汇编程序 .S 文件将它们定义并定位在固定位置。然后您可以简单地获取这些全局符号的地址,这在 constexpr 表达式中是有效的。为了使这个工作,这个全局符号必须与原始寄存器宏具有不同的名称,以防止两者之间发生冲突。

例如在您的 C++ 代码中,您将拥有:

extern volatile uint16_t TCNT1_SYMBOL;

struct foo 
  static constexpr volatile uint16_t* ptr = &TCNT1_SYMBOL;
;

然后您在项目中包含一个 .S 文件,其中包含:

#include <avr/io.h>
.global TCNT1_SYMBOL
TCNT1_SYMBOL = TCNT1

在写这篇文章时,我意识到上面的内容不仅限于 AVR-libc 案例,还可以应用于这里提出的更一般的问题。在这种情况下,您可以得到一个如下所示的 C++ 文件:

extern char MY_PTR_SYMBOL;
struct foo 
  static constexpr const void* ptr = &MY_PTR_SYMBOL;
;

auto main() -> int 
  return 0;

还有一个 .S 文件,看起来像:

.global MY_PTR_SYMBOL
MY_PTR_SYMBOL = 0x1

这是它的外观:https://godbolt.org/z/vAfaS6(不过,我不知道如何让编译器资源管理器将 cpp 和 .S 文件链接在一起

这种方法有相当多的样板,但似乎在 gcc 和 clang 版本中可靠地工作。请注意,这种方法看起来类似于使用链接器命令行选项或链接器脚本将符号放置在某个内存地址的类似方法,但是这种方法非常不便携并且难以集成到构建过程中,而上面建议的方法更便携只需在构建中添加一个 .S 文件即可。

【讨论】:

【参考方案4】:

这不是一个通用的答案,但它适用于具有固定地址的 MCU 外设的特殊功能寄存器的结构的特殊情况。联合可用于将整数转换为指针。它仍然是未定义的行为,但这种联合铸造被广泛用于嵌入式领域。它在 GCC 中完美运行(测试到 9.3.1)。

struct PeripheralRegs

    volatile uint32_t REG_A;
    volatile uint32_t REG_B;
;

template<class Base, uintptr_t Addr>
struct SFR

    union
    
        uintptr_t addr;
        Base* regs;
    ;
    constexpr SFR() :
        addr(Addr) 
    Base* operator->() const
    
        return regs;
    
    void wait_for_something() const
    
        while (!regs->REG_B);
    
;

constexpr SFR<PeripheralRegs, 0x10000000> peripheral;

uint32_t fn()

    peripheral.wait_for_something();
    return peripheral->REG_A;

【讨论】:

以上是关于constexpr 和使用重新解释强制转换的静态 const void 指针的初始化,哪个编译器是正确的?的主要内容,如果未能解决你的问题,请参考以下文章

Qt中的强制类型转换

在 c++ 逻辑层上强制转换 int

使用 constexpr 函数替代 reinterpret_cast

哪些值可以分配给 `constexpr` 引用?

C++的类型转换

2017/03/24学习笔记