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

pgky5nke  于 2022-11-12  发布在  其他
关注(0)|答案(4)|浏览(133)

请考虑以下代码片段:

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”必须由常量表达式初始化 *
    问题:
  • 根据标准,两个编译器中哪一个是正确的?
  • 如何正确地声明这种表达式?
cotxawn7

cotxawn71#

TL;DR

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

详细数据

因此,clang在这一点上是正确的,如果我们转到draft C++11 standard部分,5.19 * 常量表达式 * 段落 2 表示:
条件表达式是一个核心常量表达式,除非它包含以下内容之一作为可能计算的子表达式[...]
并包括以下项目符号:

  • 重新解释_转换(5.2.10);
    一个简单的解决方案是使用intptr_t
static constexpr intptr_t ptr = 0x1;

然后在以后需要使用它时进行转换:

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

我们可能很想就此打住,但这个故事会变得更有趣。这是已知的,但仍然是开放的gcc bug,请参见Bug 49171: [C++0x][constexpr] Constant expressions support reinterpret_cast。从讨论中可以清楚地看到,gcc开发人员对此有一些明确的用例:
我相信我在C++03中找到了常量表达式中reinterpret_cast的一致用法:

//---------------- 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
//----------------

基本上,这个程序演示了C11库函数addressof所基于的技术,因此在核心语言中从常量表达式中无条件地排除reinterpret_cast * 将使这个有用的程序无效,并使它不可能声明addressof为constexpr函数。
但无法为这些用例生成异常,请参见已关闭问题1384:
尽管C
03中允许reinterpret_cast用于地址常量表达式,但这一限制已在一些编译器中实现,并没有被证明会破坏大量代码。CWG认为处理类型发生变化的指针的复杂性(指针算术和解引用不允许在此类指针上进行)超过了放宽当前限制的可能效用。

  • 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)  ;

要找到相关文档几乎是不可能的,但是包含以下代码片段的llvm commit is informative提供了一些有趣的阅读:
支持gcc __内置常量_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测试完全折叠的参数本身。

6ljaweal

6ljaweal2#

Clang是对的,一个重解释转换的结果永远不会是一个常量表达式(参见C++115.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的值是常量表达式,这种推理基本上是不可能的。

2o7dmzc5

2o7dmzc53#

我在为AVR微控制器编程时也遇到过这个问题。Avr-libc有一些头文件(通过<avr/io.h>包含),通过定义如下宏为每个微控制器提供寄存器布局:

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

这允许使用TCNT1,就像它是一个普通变量一样,并且任何读取和写入都会自动定向到内存地址0x 84。(隐式)reinterpret_cast,它防止在常量表达式中使用此“变量”的地址。并且由于此宏是由avr-libc定义的,改变它来删除强制转换并不是一个真正的选择(重新定义这样的宏自己工作,但然后需要定义他们为所有不同的AVR芯片,复制信息从avr-libc)。
由于Shafik在这里建议的折叠hack在gcc 7及以上版本中似乎不再起作用,我一直在寻找另一种解决方案。
更仔细地查看avr-libc头文件,可以发现它们有两种模式:

  • 通常,它们定义类似变量的宏,如上所示。
  • 当在汇编程序内部使用时(或包含在_SFR_ASM_COMPAT定义中时),它们定义的宏仅包含地址,例如:#定义TCNT 1(0x 84)

乍一看,后者似乎很有用,因为您可以在include <avr/io.h>之前设置_SFR_ASM_COMPAT,然后简单地使用intptr_t常量,并直接使用地址,而不是通过指针。(下面,仅将TCNT1作为类似于宏变量或地址),这种技巧只在不包含任何其他需要类似于变量的宏的文件的源文件中起作用。实际上,这看起来不太可能(尽管可能您可以在.h文件中声明constexpr(class?)变量,并在不包含其他内容的.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文件即可。
正如评论中所指出的,尽管如此,还是有一个性能方面的缺点:在编译时,地址不再是已知的。这意味着编译器不能再使用IN、OUT、SBI、CBI、SBIC、SBIS指令。这增加了代码大小,使代码变慢,增加了寄存器压力,并且许多序列不再是原子的,因此如果需要原子执行(大多数情况下),将需要额外的代码。

siotufzp

siotufzp4#

这不是一个通用的答案,但它适用于固定地址的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;
}

相关问题