如何在 C++ class 内存结构中创建 "spacer"?
How do I create a "spacer" in a C++ class memory structure?
问题
在低级裸机嵌入式上下文中,我想在内存中创建一个空白space,在C++结构中并且没有任何名称,禁止用户访问此类内存位置。
现在,我通过放置一个丑陋的 uint32_t :96;
位域来实现它,它可以方便地代替三个单词,但是它会从 GCC 发出警告(位域太大,无法放入 uint32_t),这是非常合法的。
虽然它工作正常,但当您想要分发包含数百个警告的库时它不是很干净...
我该如何正确地做到这一点?
为什么首先会出现问题?
我正在进行的项目包括定义整个微控制器系列 (STMicroelectronics STM32) 的不同外设的内存结构。为此,结果是一个 class,它包含多个定义所有寄存器的结构的联合,具体取决于目标微控制器。
一个非常简单的外围设备的简单示例如下:通用 Input/Output (GPIO)
union
{
struct
{
GPIO_MAP0_MODER;
GPIO_MAP0_OTYPER;
GPIO_MAP0_OSPEEDR;
GPIO_MAP0_PUPDR;
GPIO_MAP0_IDR;
GPIO_MAP0_ODR;
GPIO_MAP0_BSRR;
GPIO_MAP0_LCKR;
GPIO_MAP0_AFR;
GPIO_MAP0_BRR;
GPIO_MAP0_ASCR;
};
struct
{
GPIO_MAP1_CRL;
GPIO_MAP1_CRH;
GPIO_MAP1_IDR;
GPIO_MAP1_ODR;
GPIO_MAP1_BSRR;
GPIO_MAP1_BRR;
GPIO_MAP1_LCKR;
uint32_t :32;
GPIO_MAP1_AFRL;
GPIO_MAP1_AFRH;
uint32_t :64;
};
struct
{
uint32_t :192;
GPIO_MAP2_BSRRL;
GPIO_MAP2_BSRRH;
uint32_t :160;
};
};
其中所有 GPIO_MAPx_YYY
都是宏,定义为 uint32_t :32
或寄存器类型(专用结构)。
在这里您可以看到 uint32_t :192;
效果很好,但会触发警告。
到目前为止我考虑的内容:
我可能已经用几个 uint32_t :32;
替换了它(这里是 6 个),但我有一些极端的情况,我有 uint32_t :1344;
(42)(以及其他)。所以我宁愿不在其他 8k 行之上添加大约一百行,即使结构生成是脚本化的。
确切的警告消息类似于:
width of 'sool::ll::GPIO::<anonymous union>::<anonymous struct>::<anonymous>' exceeds its type
(我就是喜欢它的阴暗)。
我宁愿不要通过简单地删除警告来解决这个问题,但是使用
#pragma GCC diagnostic push
#pragma GCC diagnostic ignored "-WTheRightFlag"
/* My code */
#pragma GCC diagnostic pop
可能是一个解决方案...如果我找到 TheRightFlag
。但是,正如 中指出的那样,gcc/cp/class.c
带有这段可悲的代码部分:
warning_at (DECL_SOURCE_LOCATION (field), 0,
"width of %qD exceeds its type", field);
这告诉我们没有 -Wxxx
标志来删除此警告...
在嵌入式系统领域,您可以通过使用结构或通过定义指向寄存器地址的指针来为硬件建模。
不建议按结构建模,因为允许编译器在成员之间添加填充以进行对齐(尽管嵌入式系统的许多编译器都有打包结构的编译指示)。
示例:
uint16_t * const UART1 = (uint16_t *)(0x40000);
const unsigned int UART_STATUS_OFFSET = 1U;
const unsigned int UART_TRANSMIT_REGISTER = 2U;
uint16_t * const UART1_STATUS_REGISTER = (UART1 + UART_STATUS_OFFSET);
uint16_t * const UART1_TRANSMIT_REGISTER = (UART1 + UART_TRANSMIT_REGISTER);
您也可以使用数组表示法:
uint16_t status = UART1[UART_STATUS_OFFSET];
如果您必须使用结构,恕我直言,跳过地址的最佳方法是定义一个成员而不访问它:
struct UART1
{
uint16_t status;
uint16_t reserved1; // Transmit register
uint16_t receive_register;
};
在我们的一个项目中,我们有来自不同供应商的常量和结构(供应商 1 使用常量,而供应商 2 使用结构)。
使用多个相邻的匿名位域。所以而不是:
uint32_t :160;
例如,您有:
uint32_t :32;
uint32_t :32;
uint32_t :32;
uint32_t :32;
uint32_t :32;
每个您想匿名的注册一个。
如果要填充的 space 较大,则使用宏重复单个 32 位 space 可能会更清晰且不易出错。例如,给定:
#define REPEAT_2(a) a a
#define REPEAT_4(a) REPEAT_2(a) REPEAT_2(a)
#define REPEAT_8(a) REPEAT_4(a) REPEAT_4(a)
#define REPEAT_16(a) REPEAT_8(a) REPEAT_8(a)
#define REPEAT_32(a) REPEAT_16(a) REPEAT_16(a)
然后可以加一个1344(42 * 32位)space这样:
struct
{
...
REPEAT_32(uint32_t :32;)
REPEAT_8(uint32_t :32;)
REPEAT_2(uint32_t :32;)
...
};
C++ 式的方式怎么样?
namespace GPIO {
static volatile uint32_t &MAP0_MODER = *reinterpret_cast<uint32_t*>(0x4000);
static volatile uint32_t &MAP0_OTYPER = *reinterpret_cast<uint32_t*>(0x4004);
}
int main() {
GPIO::MAP0_MODER = 42;
}
由于 GPIO
命名空间,您可以自动完成,不需要虚拟填充。甚至,更清楚是怎么回事,因为你可以看到每个寄存器的地址,你完全不必依赖编译器的填充行为。
要扩展 Clifford 的回答,您始终可以宏出匿名位域。
所以
uint32_t :160;
使用
#define EMPTY_32_1 \
uint32_t :32
#define EMPTY_32_2 \
uint32_t :32; \ // I guess this also can be replaced with uint64_t :64
uint32_t :32
#define EMPTY_32_3 \
uint32_t :32; \
uint32_t :32; \
uint32_t :32
#define EMPTY_UINT32(N) EMPTY_32_ ## N
然后像这样使用它
struct A {
EMPTY_UINT32(3);
/* which resolves to EMPTY_32_3, which then resolves to real declarations */
}
不幸的是,您需要的 EMPTY_32_X
变体与您拥有的字节数一样多:(
尽管如此,它仍允许您在结构中进行单一声明。
扩展@Clifford 和@Adam Kotwasinski 的回答:
#define REP10(a) a a a a a a a a a a
#define REP1034(a) REP10(REP10(REP10(a))) REP10(a a a) a a a a
struct foo {
int before;
REP1034(unsigned int :32;)
int after;
};
int main(void){
struct foo bar;
return 0;
}
将大间隔定义为 32 位组。
#define M_32(x) M_2(M_16(x))
#define M_16(x) M_2(M_8(x))
#define M_8(x) M_2(M_4(x))
#define M_4(x) M_2(M_2(x))
#define M_2(x) x x
#define SPACER int : 32;
struct {
M_32(SPACER) M_8(SPACER) M_4(SPACER)
};
反解。
不要这样做:混合私有和 public 字段。
也许带有生成唯一变量名的计数器的宏会有用?
#define CONCAT_IMPL( x, y ) x##y
#define MACRO_CONCAT( x, y ) CONCAT_IMPL( x, y )
#define RESERVED MACRO_CONCAT(Reserved_var, __COUNTER__)
struct {
GPIO_MAP1_CRL;
GPIO_MAP1_CRH;
GPIO_MAP1_IDR;
GPIO_MAP1_ODR;
GPIO_MAP1_BSRR;
GPIO_MAP1_BRR;
GPIO_MAP1_LCKR;
private:
char RESERVED[4];
public:
GPIO_MAP1_AFRL;
GPIO_MAP1_AFRH;
private:
char RESERVED[8];
};
geza 说得对,您真的不想为此使用 classes。
但是,如果您坚持要添加 n 字节宽度的未使用成员,最好的方法就是这样做:
char unused[n];
如果您添加特定于实现的 pragma 以防止向 class 的成员添加任意填充,则此方法可行。
对于 GNU C/C++(gcc、clang 和其他支持相同扩展的软件),放置该属性的有效位置之一是:
#include <stddef.h>
#include <stdint.h>
#include <assert.h> // for C11 static_assert, so this is valid C as well as C++
struct __attribute__((packed)) GPIO {
volatile uint32_t a;
char unused[3];
volatile uint32_t b;
};
static_assert(offsetof(struct GPIO, b) == 7, "wrong GPIO struct layout");
(示例 on the Godbolt compiler explorer 显示 offsetof(GPIO, b)
= 7 个字节。)
我认为引入更多的结构是有益的;反过来,这可能会解决垫片的问题。
命名变体
虽然平面命名空间很好,但问题是您最终会得到杂乱无章的 collection 字段,并且没有简单的方法将所有相关字段一起传递。此外,通过在匿名联合中使用匿名结构,您不能将引用传递给结构本身,也不能将它们用作模板参数。
因此,作为第一步,我会考虑 打破 struct
:
// GpioMap0.h
#pragma once
// #includes
namespace Gpio {
struct Map0 {
GPIO_MAP0_MODER;
GPIO_MAP0_OTYPER;
GPIO_MAP0_OSPEEDR;
GPIO_MAP0_PUPDR;
GPIO_MAP0_IDR;
GPIO_MAP0_ODR;
GPIO_MAP0_BSRR;
GPIO_MAP0_LCKR;
GPIO_MAP0_AFR;
GPIO_MAP0_BRR;
GPIO_MAP0_ASCR;
};
} // namespace Gpio
// GpioMap1.h
#pragma once
// #includes
namespace Gpio {
struct Map1 {
// fields
};
} // namespace Gpio
// ... others headers ...
最后,全局 header:
// Gpio.h
#pragma once
#include "GpioMap0.h"
#include "GpioMap1.h"
// ... other headers ...
namespace Gpio {
union Gpio {
Map0 map0;
Map1 map1;
// ... others ...
};
} // namespace Gpio
现在,我可以写一个 void special_map0(Gpio:: Map0 volatile& map);
,并且可以快速概览所有可用的架构。
简单的垫片
将定义拆分为多个 header 后,header 更易于管理。
因此,我最初要完全满足您的要求的方法是坚持重复 std::uint32_t:32;
。是的,它在现有的 8k 行的基础上增加了几条 100 行,但由于每个 header 单独较小,因此可能不会那么糟糕。
如果您愿意考虑更奇特的解决方案,不过...
介绍 $.
little-known事实是 $
是 C++ 标识符的可行字符;它甚至是一个可行的起始字符(与数字不同)。
A $
出现在源代码中可能会引起注意,而 $$$$
肯定会在代码审查期间引起注意。这是您可以轻松利用的东西:
#define GPIO_RESERVED(Index_, N_) std::uint32_t $$$$##Index_[N_];
struct Map3 {
GPIO_RESERVED(0, 6);
GPIO_MAP2_BSRRL;
GPIO_MAP2_BSRRH;
GPIO_RESERVED(1, 5);
};
您甚至可以将一个简单的 "lint" 放在一起作为 pre-commit 挂钩或在您的 CI 中查找已提交的 C++ 代码中的 $$$$
并拒绝此类提交.
虽然我同意结构不应该用于 MCU I/O 端口访问,但原始问题可以这样回答:
struct __attribute__((packed)) test {
char member1;
char member2;
volatile struct __attribute__((packed))
{
private:
volatile char spacer_bytes[7];
} spacer;
char member3;
char member4;
};
您可能需要将 __attribute__((packed))
替换为 #pragma pack
或类似内容,具体取决于您的编译器语法。
在结构中混合使用私有成员和 public 成员通常会导致 C++ 标准不再保证内存布局。
但是,如果结构的 所有 非静态成员都是私有的,它仍然被认为是 POD / 标准布局,嵌入它们的结构也是如此。
出于某种原因,如果匿名结构的成员是私有的,gcc 会发出警告,因此我必须为其命名。或者,将其包装到另一个匿名结构中也可以消除警告(这可能是一个错误)。
请注意 spacer
成员本身不是私有的,因此仍然可以通过这种方式访问数据:
(char*)(void*)&testobj.spacer;
然而,这样的表达方式看起来像是一个明显的 hack,希望在没有真正充分理由的情况下不会被使用,更不用说是错误了。
问题
在低级裸机嵌入式上下文中,我想在内存中创建一个空白space,在C++结构中并且没有任何名称,禁止用户访问此类内存位置。
现在,我通过放置一个丑陋的 uint32_t :96;
位域来实现它,它可以方便地代替三个单词,但是它会从 GCC 发出警告(位域太大,无法放入 uint32_t),这是非常合法的。
虽然它工作正常,但当您想要分发包含数百个警告的库时它不是很干净...
我该如何正确地做到这一点?
为什么首先会出现问题?
我正在进行的项目包括定义整个微控制器系列 (STMicroelectronics STM32) 的不同外设的内存结构。为此,结果是一个 class,它包含多个定义所有寄存器的结构的联合,具体取决于目标微控制器。
一个非常简单的外围设备的简单示例如下:通用 Input/Output (GPIO)
union
{
struct
{
GPIO_MAP0_MODER;
GPIO_MAP0_OTYPER;
GPIO_MAP0_OSPEEDR;
GPIO_MAP0_PUPDR;
GPIO_MAP0_IDR;
GPIO_MAP0_ODR;
GPIO_MAP0_BSRR;
GPIO_MAP0_LCKR;
GPIO_MAP0_AFR;
GPIO_MAP0_BRR;
GPIO_MAP0_ASCR;
};
struct
{
GPIO_MAP1_CRL;
GPIO_MAP1_CRH;
GPIO_MAP1_IDR;
GPIO_MAP1_ODR;
GPIO_MAP1_BSRR;
GPIO_MAP1_BRR;
GPIO_MAP1_LCKR;
uint32_t :32;
GPIO_MAP1_AFRL;
GPIO_MAP1_AFRH;
uint32_t :64;
};
struct
{
uint32_t :192;
GPIO_MAP2_BSRRL;
GPIO_MAP2_BSRRH;
uint32_t :160;
};
};
其中所有 GPIO_MAPx_YYY
都是宏,定义为 uint32_t :32
或寄存器类型(专用结构)。
在这里您可以看到 uint32_t :192;
效果很好,但会触发警告。
到目前为止我考虑的内容:
我可能已经用几个 uint32_t :32;
替换了它(这里是 6 个),但我有一些极端的情况,我有 uint32_t :1344;
(42)(以及其他)。所以我宁愿不在其他 8k 行之上添加大约一百行,即使结构生成是脚本化的。
确切的警告消息类似于:
width of 'sool::ll::GPIO::<anonymous union>::<anonymous struct>::<anonymous>' exceeds its type
(我就是喜欢它的阴暗)。
我宁愿不要通过简单地删除警告来解决这个问题,但是使用
#pragma GCC diagnostic push
#pragma GCC diagnostic ignored "-WTheRightFlag"
/* My code */
#pragma GCC diagnostic pop
可能是一个解决方案...如果我找到 TheRightFlag
。但是,正如 gcc/cp/class.c
带有这段可悲的代码部分:
warning_at (DECL_SOURCE_LOCATION (field), 0,
"width of %qD exceeds its type", field);
这告诉我们没有 -Wxxx
标志来删除此警告...
在嵌入式系统领域,您可以通过使用结构或通过定义指向寄存器地址的指针来为硬件建模。
不建议按结构建模,因为允许编译器在成员之间添加填充以进行对齐(尽管嵌入式系统的许多编译器都有打包结构的编译指示)。
示例:
uint16_t * const UART1 = (uint16_t *)(0x40000);
const unsigned int UART_STATUS_OFFSET = 1U;
const unsigned int UART_TRANSMIT_REGISTER = 2U;
uint16_t * const UART1_STATUS_REGISTER = (UART1 + UART_STATUS_OFFSET);
uint16_t * const UART1_TRANSMIT_REGISTER = (UART1 + UART_TRANSMIT_REGISTER);
您也可以使用数组表示法:
uint16_t status = UART1[UART_STATUS_OFFSET];
如果您必须使用结构,恕我直言,跳过地址的最佳方法是定义一个成员而不访问它:
struct UART1
{
uint16_t status;
uint16_t reserved1; // Transmit register
uint16_t receive_register;
};
在我们的一个项目中,我们有来自不同供应商的常量和结构(供应商 1 使用常量,而供应商 2 使用结构)。
使用多个相邻的匿名位域。所以而不是:
uint32_t :160;
例如,您有:
uint32_t :32;
uint32_t :32;
uint32_t :32;
uint32_t :32;
uint32_t :32;
每个您想匿名的注册一个。
如果要填充的 space 较大,则使用宏重复单个 32 位 space 可能会更清晰且不易出错。例如,给定:
#define REPEAT_2(a) a a
#define REPEAT_4(a) REPEAT_2(a) REPEAT_2(a)
#define REPEAT_8(a) REPEAT_4(a) REPEAT_4(a)
#define REPEAT_16(a) REPEAT_8(a) REPEAT_8(a)
#define REPEAT_32(a) REPEAT_16(a) REPEAT_16(a)
然后可以加一个1344(42 * 32位)space这样:
struct
{
...
REPEAT_32(uint32_t :32;)
REPEAT_8(uint32_t :32;)
REPEAT_2(uint32_t :32;)
...
};
C++ 式的方式怎么样?
namespace GPIO {
static volatile uint32_t &MAP0_MODER = *reinterpret_cast<uint32_t*>(0x4000);
static volatile uint32_t &MAP0_OTYPER = *reinterpret_cast<uint32_t*>(0x4004);
}
int main() {
GPIO::MAP0_MODER = 42;
}
由于 GPIO
命名空间,您可以自动完成,不需要虚拟填充。甚至,更清楚是怎么回事,因为你可以看到每个寄存器的地址,你完全不必依赖编译器的填充行为。
要扩展 Clifford 的回答,您始终可以宏出匿名位域。
所以
uint32_t :160;
使用
#define EMPTY_32_1 \
uint32_t :32
#define EMPTY_32_2 \
uint32_t :32; \ // I guess this also can be replaced with uint64_t :64
uint32_t :32
#define EMPTY_32_3 \
uint32_t :32; \
uint32_t :32; \
uint32_t :32
#define EMPTY_UINT32(N) EMPTY_32_ ## N
然后像这样使用它
struct A {
EMPTY_UINT32(3);
/* which resolves to EMPTY_32_3, which then resolves to real declarations */
}
不幸的是,您需要的 EMPTY_32_X
变体与您拥有的字节数一样多:(
尽管如此,它仍允许您在结构中进行单一声明。
扩展@Clifford 和@Adam Kotwasinski 的回答:
#define REP10(a) a a a a a a a a a a
#define REP1034(a) REP10(REP10(REP10(a))) REP10(a a a) a a a a
struct foo {
int before;
REP1034(unsigned int :32;)
int after;
};
int main(void){
struct foo bar;
return 0;
}
将大间隔定义为 32 位组。
#define M_32(x) M_2(M_16(x))
#define M_16(x) M_2(M_8(x))
#define M_8(x) M_2(M_4(x))
#define M_4(x) M_2(M_2(x))
#define M_2(x) x x
#define SPACER int : 32;
struct {
M_32(SPACER) M_8(SPACER) M_4(SPACER)
};
反解。
不要这样做:混合私有和 public 字段。
也许带有生成唯一变量名的计数器的宏会有用?
#define CONCAT_IMPL( x, y ) x##y
#define MACRO_CONCAT( x, y ) CONCAT_IMPL( x, y )
#define RESERVED MACRO_CONCAT(Reserved_var, __COUNTER__)
struct {
GPIO_MAP1_CRL;
GPIO_MAP1_CRH;
GPIO_MAP1_IDR;
GPIO_MAP1_ODR;
GPIO_MAP1_BSRR;
GPIO_MAP1_BRR;
GPIO_MAP1_LCKR;
private:
char RESERVED[4];
public:
GPIO_MAP1_AFRL;
GPIO_MAP1_AFRH;
private:
char RESERVED[8];
};
geza 说得对,您真的不想为此使用 classes。
但是,如果您坚持要添加 n 字节宽度的未使用成员,最好的方法就是这样做:
char unused[n];
如果您添加特定于实现的 pragma 以防止向 class 的成员添加任意填充,则此方法可行。
对于 GNU C/C++(gcc、clang 和其他支持相同扩展的软件),放置该属性的有效位置之一是:
#include <stddef.h>
#include <stdint.h>
#include <assert.h> // for C11 static_assert, so this is valid C as well as C++
struct __attribute__((packed)) GPIO {
volatile uint32_t a;
char unused[3];
volatile uint32_t b;
};
static_assert(offsetof(struct GPIO, b) == 7, "wrong GPIO struct layout");
(示例 on the Godbolt compiler explorer 显示 offsetof(GPIO, b)
= 7 个字节。)
我认为引入更多的结构是有益的;反过来,这可能会解决垫片的问题。
命名变体
虽然平面命名空间很好,但问题是您最终会得到杂乱无章的 collection 字段,并且没有简单的方法将所有相关字段一起传递。此外,通过在匿名联合中使用匿名结构,您不能将引用传递给结构本身,也不能将它们用作模板参数。
因此,作为第一步,我会考虑 打破 struct
:
// GpioMap0.h
#pragma once
// #includes
namespace Gpio {
struct Map0 {
GPIO_MAP0_MODER;
GPIO_MAP0_OTYPER;
GPIO_MAP0_OSPEEDR;
GPIO_MAP0_PUPDR;
GPIO_MAP0_IDR;
GPIO_MAP0_ODR;
GPIO_MAP0_BSRR;
GPIO_MAP0_LCKR;
GPIO_MAP0_AFR;
GPIO_MAP0_BRR;
GPIO_MAP0_ASCR;
};
} // namespace Gpio
// GpioMap1.h
#pragma once
// #includes
namespace Gpio {
struct Map1 {
// fields
};
} // namespace Gpio
// ... others headers ...
最后,全局 header:
// Gpio.h
#pragma once
#include "GpioMap0.h"
#include "GpioMap1.h"
// ... other headers ...
namespace Gpio {
union Gpio {
Map0 map0;
Map1 map1;
// ... others ...
};
} // namespace Gpio
现在,我可以写一个 void special_map0(Gpio:: Map0 volatile& map);
,并且可以快速概览所有可用的架构。
简单的垫片
将定义拆分为多个 header 后,header 更易于管理。
因此,我最初要完全满足您的要求的方法是坚持重复 std::uint32_t:32;
。是的,它在现有的 8k 行的基础上增加了几条 100 行,但由于每个 header 单独较小,因此可能不会那么糟糕。
如果您愿意考虑更奇特的解决方案,不过...
介绍 $.
little-known事实是 $
是 C++ 标识符的可行字符;它甚至是一个可行的起始字符(与数字不同)。
A $
出现在源代码中可能会引起注意,而 $$$$
肯定会在代码审查期间引起注意。这是您可以轻松利用的东西:
#define GPIO_RESERVED(Index_, N_) std::uint32_t $$$$##Index_[N_];
struct Map3 {
GPIO_RESERVED(0, 6);
GPIO_MAP2_BSRRL;
GPIO_MAP2_BSRRH;
GPIO_RESERVED(1, 5);
};
您甚至可以将一个简单的 "lint" 放在一起作为 pre-commit 挂钩或在您的 CI 中查找已提交的 C++ 代码中的 $$$$
并拒绝此类提交.
虽然我同意结构不应该用于 MCU I/O 端口访问,但原始问题可以这样回答:
struct __attribute__((packed)) test {
char member1;
char member2;
volatile struct __attribute__((packed))
{
private:
volatile char spacer_bytes[7];
} spacer;
char member3;
char member4;
};
您可能需要将 __attribute__((packed))
替换为 #pragma pack
或类似内容,具体取决于您的编译器语法。
在结构中混合使用私有成员和 public 成员通常会导致 C++ 标准不再保证内存布局。 但是,如果结构的 所有 非静态成员都是私有的,它仍然被认为是 POD / 标准布局,嵌入它们的结构也是如此。
出于某种原因,如果匿名结构的成员是私有的,gcc 会发出警告,因此我必须为其命名。或者,将其包装到另一个匿名结构中也可以消除警告(这可能是一个错误)。
请注意 spacer
成员本身不是私有的,因此仍然可以通过这种方式访问数据:
(char*)(void*)&testobj.spacer;
然而,这样的表达方式看起来像是一个明显的 hack,希望在没有真正充分理由的情况下不会被使用,更不用说是错误了。