考虑以下代码:
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'必须用常量表达式初始化
问题:
根据标准,哪两个编译器是正确的?
宣布这种表达方式的正确方法是什么?
答案 0 :(得分:14)
TL; DR
clang
是正确的,这是已知的gcc
错误。您可以使用intptr_t
代替并在需要使用该值时进行投射,或者如果这不可行,那么gcc
和clang
都支持一些记录的变通方法,应该允许您的特定用例。
详细
如果我们转到draft C++11 standard部分clang
常量表达式段落 2 ,那么5.19
对此是正确的:
条件表达式是核心常量表达式,除非它 涉及以下之一作为潜在评估的子表达式 [...]
并包含以下项目符号:
- reinterpret_cast(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 无条件来自核心语言中的常量表达式会使这个有用的程序无效并且无法实现 将addressof声明为constexpr函数。
但无法针对这些用例进行异常处理,请参阅closed issues 1384:
虽然地址常量允许reinterpret_cast 在C ++ 03中的表达式中,这种限制已在某些实现中实现 编译器并没有证明打破了大量的代码。 CWG 认为处理指针的并发症 已更改(无法允许指针算术和取消引用 这样的指针)超过了放松电流的可能效用 限制。
但显然gcc
和clang
支持一个小文档扩展,允许使用__builtin_constant_p (exp)不断折叠非常量表达式,因此接受以下表达式gcc
和clang
:
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 __builtin_constant_p()? ......:...在C ++ 11中折叠黑客
和
+ // __builtin_constant_p? :是神奇的,永远是一个潜在的常数。
和
- //这个宏强制它的参数是常量折叠的,即使它不是
- //否则为常量表达式。
定义fold(x)(__ builtin_constant_p(x)?(x):( x))
我们可以在gcc-patches电子邮件中找到有关此功能的更正式的说明:C constant expressions, VLAs etc. fixes说:
此外,__ builtin_constant_p的规则调用为条件 实现中的表达条件比那些更宽松 在正式模型中:选定的一半条件表达式 是完全折叠的,不管它是否正式是一个常数 表达式,因为__builtin_constant_p测试一个完全折叠的参数 本身。
答案 1 :(得分:11)
Clang是对的。重新解释的结果永远不是一个常数表达式(参见C ++ 11 5.19 / 2)。
常量表达式的目的是可以将它们作为值进行推理,并且值必须是有效的。你写的东西不是一个有效的指针(因为它不是一个对象的地址,或者是通过指针算法与一个对象的地址相关),所以你不允许使用它它作为一个不变的表达。如果您只想存储数字1
,请将其存储为uintptr_t
并在使用网站上重新解释广告。
顺便说一句,要详细说明&#34;有效指针&#34;的概念,请考虑以下constexpr
指针:
int const a[10] = { 1 };
constexpr int * p1 = a + 5;
constexpr int b[10] = { 2 };
constexpr int const * p2 = b + 10;
// constexpr int const * p3 = b + 11; // Error, not a constant expression
// static_assert(*p1 == 0, "") // Error, not a constant expression
static_assert(p2[-2] == 0, ""); // OK
// static_assert(p2[1] == 0, ""); // Error, "p2[2] would have UB"
static_assert(p2 != nullptr, ""); // OK
// static_assert(p2 + 1 != nullptr, ""); // Error, "p2 + 1 would have UB"
p1
和p2
都是常量表达式。但是指针运算的结果是否是常量表达式取决于它是否不是UB!如果您允许reinterpret_casts的值为常量表达式,那么这种推理基本上是不可能的。
答案 2 :(得分:1)
在为AVR微控制器编程时,我也一直遇到这个问题。 Avr-libc具有头文件(通过<avr/io.h>
包含),这些头文件通过定义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
中时),它们定义仅包含地址的宏,例如:
#定义TCNT1(0x84)
乍一看,后者似乎很有用,因为您可以在包含_SFR_ASM_COMPAT
之前设置<avr/io.h>
,然后简单地使用intptr_t
常量并直接使用地址,而不是通过指针。但是,由于您只能包含avr-libc标头一次(现在,TCNT1
只能是类似变量的宏或地址),因此该技巧仅适用于不包含任何内容的源文件其他需要类似macro-macros的文件。在实践中,这似乎不太可能(尽管也许可以在.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文件即可。
答案 3 :(得分:0)
这不是一个通用的答案,但它适用于具有固定地址的 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;
}