使用绝对指针地址作为模板参数

using an absolute pointer address as a template argument

本文关键字:参数 地址 指针      更新时间:2023-10-16

我有一个模板类,它的第一个模板参数是foo *指针。我想用位于绝对地址的 foo 实例化其中一个,如下所示:

class foo
{
    int baz;
};
template<foo *f> class bar
{
public:
    bar() {}
    void update() { /* ... */ }
};
// ....
#define FOO_ADDR ((foo *)0x80103400)
#define FOO_NULL ((foo *)0)
foo testFoo;
bar<FOO_ADDR> myFoo;        // fails with non-integral argument
bar<FOO_NULL> huh;          // compiles, I was surprised by this
bar<&testFoo> test;         // compiles as expected (but not useful)

有谁知道是否可以不求助于链接器并使用外部链接定义FOO_ADDR?

这是在Keil ARM C/C++编译器版本V5.06更新1(build 61(中,我尝试打开C++11模式,但是(除了在系统标头中抛出大量新错误(它没有改变行为。

更新:这是使用 int 强制转换的建议解决方案(这次使用真实代码(

template<uint32 PORT, uint32 BIT, uint32 RATE> class LedToggle
{
    uint32 mTicks;
    uint32 mSetReset;
    public:
    LedToggle()
    {
        mTicks = 0;
        mSetReset = 1 << BIT;
    }
    void Update()
    {
        uint32 mask = ((mTicks++ & RATE) - 1) >> 31;
        ((GPIO_TypeDef *)PORT)->BSRR = mSetReset & mask;
        mSetReset ^= ((1 << BIT) | (1 << (BIT + 16))) & mask;
    }
};
LedToggle<(uint32)GPIOC, 13, 1023> led;

它很丑陋,但它确实有效。我很想听听是否有人可以改进它?

声明bar<(foo*)0x80103400> myFoo;格式不正确,因为非类型模板参数必须是来自 [temp.arg.nontype] 的常量表达式:

非类型模板参数的模板参数

应是模板参数类型的转换常量表达式 (5.20(。

你传递的论点不是,来自 [expr.const]:

条件表达式 e 是核心常量表达式,除非 e 的计算遵循 抽象机器 (1.9( 将计算以下表达式之一:
— [...]
reinterpret_cast(5.2.10(;
— [...]

声明bar<(foo*)0> huh起作用,因为它不涉及强制转换,它只是一个类型 foo* 的空指针(0是特殊的(,因此它是一个有效的常量表达式。

<小时 />

您可以简单地将地址作为模板非类型参数传入:

template <uintptr_t address>
struct bar { ... };
bar<0x8013400> myFooWorks;

这是可行的。

面对同样的问题(在 STM32 上(,作为解决方法,我找到了函数指针模板参数,如下所示:

template<GPIO_TypeDef* PORT(), uint32 BIT, uint32 RATE>
class LedToggle
{
    public:
    void Update()
    {
        // ...
        PORT()->BSRR = mSetReset & mask;
        // ...
    }
};
constexpr GPIO_TypeDef* Port_C() {
  return PORTC;
}
LedToggle<Port_C, 13, 1023> led;

请注意,我们使用函数指针作为模板参数,指向返回所需实际指针的函数。在该函数内部允许强制转换;由于函数是constexpr声明的,编译器可以(应该(优化实际的函数调用,并像文字一样使用函数的返回值。

向/从 ints 进行转换是有效的,但正如所指出的,这很危险。另一个类似于 JimmyB 的解决方案是使用枚举类而不是函数指针。枚举类成员值设置为供应商提供的标头中指定的设备地址。例如,对于STM32系列,意法半导体提供了一个定义如下的接头:

// Vendor-supplied device header file (example)
#define GPIOA_BASE = 0x40001000
#define GPIOB_BASE = 0x40002000
//    etc...

在代码中,创建一个枚举类:

#include <vendor-supplied-device-header.h>
enum class GPIO : uint32_t {
    A = GPIOA_BASE, 
    B = GPIOB_BASE, 
    C = GPIOC_BASE, 
    D = GPIOD_BASE, 
    E = GPIOE_BASE,
    F = GPIOF_BASE,
    G = GPIOG_BASE,
    #ifdef GPIOH_BASE   //optional: wrap each member in an #ifdef to improve portability
    H = GPIOH_BASE,
    #endif
    //.. etc
};

为避免多个混乱的转换,只需使用私有方法在类中执行一次即可。例如,你的 LedToggle 类会这样写:

template<GPIOPORT PORT, uint8_t PIN, uint32_t RATE> class LedToggle
{
    static_assert(PIN < 15, "Only pin numbers 0 - 15 are valid");
    volatile auto GPIOPort(GPIOPORT PORT) {
        return reinterpret_cast<GPIO_TypeDef *>(port_);
    }
    uint32_t mTicks;
    uint32_t mSetReset;
    public:
    LedToggle()
    {
        mTicks = 0;
        mSetReset = 1 << PIN;
    }
    void Update()
    {
        uint32 mask = ((mTicks++ & RATE) - 1) >> 31;
        GPIOPort(PORT)->BSRR = mSetReset & mask;
        mSetReset ^= ((1 << PIN) | (1 << (PIN + 16))) & mask;
    }
};
LedToggle<GPIO::C, 13, 1023> led;

此方法的好处是类用户被迫仅使用 GPIO 枚举类的成员,因此禁止无效地址。

您可以将

枚举类用于任何模板参数,例如,您可以将 PIN 参数替换为枚举类,其成员设置为供应商指定的GPIO_PIN_1、GPIO_PIN_2等。然后你会写:

LedToggle<GPIO::C, Pin::_13, 1023> 

来自 C++11 标准(强调我的(:

14.3.2 模板非类型参数

1 非类型、非模板模板参数的模板参数应为以下参数之一:

— 对于整型或枚举类型的非类型模板参数,模板参数类型的转换常量表达式 (5.19(;

— 非类型模板参数的名称;或

— 一个常量表达式 (5.19(,它指定具有静态存储持续时间和外部或内部链接的对象或具有外部或内部链接的函数的地址,包括函数模板和函数模板 ID,但不包括非静态类成员,表示(忽略括号(& id 表达式,但如果名称引用函数或数组,则可以省略&,如果模板参数是一个参考;或

— 计算结果为 null 指针值 (4.10( 的常量表达式;或

使用 g++ -Wall 编译时,出现以下错误:

error: ‘2148545536u’ is not a valid template argument for ‘foo*’ because it is not the address of a variable
    bar<FOO_ADDR> myFoo;        // fails with non-integral argument
                       ^ 

编译器似乎能够检测到0x80103400不是变量的地址,它只是一个常量表达式。

您是否尝试过将指针地址转换为uintptr_t,这是一个能够保存指针值的无符号整数?该类型在标准标头<cstdint>中定义,但它只是可选的。如果您的 C++ 版本中不存在它,请尝试改为size_t

然后,完整的示例如下所示:

#include <cstdint>
class foo
{
    int baz;
};
template<uintptr_t addr> class bar
{
    constexpr static const foo* f = (foo*)(addr);
public:
    bar() {}
    void update() {  }
};
#define FOO_ADDR ((uintptr_t)0x80103400)
int main() {
    bar<FOO_ADDR> myFoo;
}

明显的缺点是模板参数没有类型检查。您传递一个值,该值有望引用foo对象而不是另一个对象。

更不用说就标准而言,

我们处于未定义的行为世界中......


你似乎可以用这条线编译

constexpr static const foo* f = reinterpret_cast<foo*>(addr);

至少还有一些编译器(http://coliru.stacked-crooked.com/a/5af62bedecf2d75a(


如果您的编译器在 constexpr 上下文中拒绝强制转换,因为它格式不正确(根据 Barry 的评论(,您可以将其定义为常规static const变量:

template<uintptr_t addr>
class bar
{
    static const foo* f;
public:
    bar() {}
    void update() {  }
};
template<uintptr_t addr>
const foo* bar<addr>::f = reinterpret_cast<foo*>(addr);

不太理想,但有望解决这个问题。

首先,不要使用 c 样式转换。它没有明确说明是什么。

其次,您将意识到,为了将任意数字转换为指针,您需要使用 reinterpret_cast 。这种强制转换在 constexpr 表达式中是不允许的。

由于模板参数只能使用常量表达式,因此不可能使用任意数字作为指针。

可能为 0

的情况是因为 0 可转换为 nullptr,这是常量表达式。