以 constexpr 方式设计 class

Designing class in a constexpr manner

我不确定如何询问我的问题,因为这是一连串的决定,我不确定在什么时候我应该做不同的事情,所以让我充分解释我的情况然后提出问题。

我在嵌入式设备上有一个小型 GPIO 驱动程序,经过精简和简化后的代码如下所示:

https://godbolt.org/z/8GP7Y53rc

#include <cstdint>

namespace GPIO {

    enum class port_e:uint32_t {
        io0  = 0,
        io1  = 1,
        io2  = 2,
        io3  = 3,
        io4  = 4,
        io5  = 5,
        io6  = 6,
        io7  = 7,
    };

    enum class portMode_e:uint32_t {
        input  = 0b10,
        output = 0b01,
    };

    struct instance_s {
        uintptr_t baseAddress;
        uint32_t  numberOfIOs;
    };

    template<uint32_t baseAddress, uint32_t numberOfIOs>
    constexpr instance_s makeInstance() {
        // Check for valid base address
        static_assert((baseAddress % 4) == 0,                                          "The GPIO base port address needs to be 32-bit aligned");
        static_assert((baseAddress >= 0x7000'0000UL) && (baseAddress < 0x8000'2000UL), "The GPIO has to be within the APB bus range");

        // Check for valid number of IOs
        static_assert(numberOfIOs>0 && numberOfIOs<=8,                                 "The number of IOs in this GPIO port_s needs to be between 1-32");

        return {baseAddress, numberOfIOs};
    }

    template<const instance_s* thisGpio, port_e portId>
    void config(portMode_e mode) {
        static_assert(thisGpio->numberOfIOs > static_cast<uint32_t>(portId), "You are trying to access IOs above your numberOfIos configured in the instance");

        *(volatile uint32_t *)(thisGpio->baseAddress + static_cast<uint32_t>(portId)) = static_cast<uint32_t>(mode);
    }

}

constexpr auto gpioOut = GPIO::makeInstance<0x7000'5000UL, 2>();

int main(void) {
    GPIO::config<&gpioOut, GPIO::port_e::io0>(GPIO::portMode_e::output);
    GPIO::config<&gpioOut, GPIO::port_e::io1>(GPIO::portMode_e::output);

    return 0u;
}

一切正常。我不得不使用模板参数,因为我希望代码在编译时完成并允许 static_asserts 验证参数的值。

我创建了一个 instance_s 结构来保存有关我当前 GPIO 设备的信息(基地址和它有多少个引脚)然后使用 GPIO 的每个命令都需要instance_s 结构在编译时可以执行特定 GPIO 实例的代码。我唯一的问题是有人可以直接创建实例 instance_s 结构而不调用 makeInstance 并且会跳过验证断言。

所以我想通过将命名空间更改为 class 并使 [=79 来保护 instance_s 结构类型=] 结构私有所以 makeInstance 将是获取实例的唯一方法,所有调用都将使用内部 instance_s 在 class 内部,所以他们不需要在外部传递 instance_s 结构。进程中 makeInstance 而不是返回 instance_s 结构返回 class 的实例。现在我必须在 class 中保留 instance_s 结构,所以创建了一个字段但是因为我不能在那一刻初始化它,所以我可以'没有它作为constexpr。

无法将模板参数与 class 构造函数一起使用,因此我无法在其中使用 static_asserts,因此静态 makeInstance static_assert 方法现在不是返回 instance_s 结构它 returns class 的实例,而构造函数现在必须是私有的,因为它只接受参数并填充内部 instance_s 结构。在这样做的过程中,我失去了整个事情的概念。现在我的 stat_assert 失败了:error: non-constant condition for static assertion

我知道,因为字段 thisGpio 持有我的 instance_s 结构不是 constexpr,我找不到使它成为一体的方法,同时实现我想要的。

https://godbolt.org/z/qrn8oEdza

#include <cstdint>

class GPIO {

public:
    enum class port_e:uint32_t {
        io0  = 0,
        io1  = 1,
        io2  = 2,
        io3  = 3,
        io4  = 4,
        io5  = 5,
        io6  = 6,
        io7  = 7,
    };

    enum class portMode_e:uint32_t {
        input  = 0b10,
        output = 0b01,
    };

private:
    const uintptr_t baseAddress;
    const uint32_t  numberOfIOs;

    GPIO(uint32_t baseAddress, uint32_t numberOfIOs): 
        baseAddress(baseAddress), numberOfIOs(numberOfIOs) {}

public:
    template<uint32_t baseAddress, uint32_t numberOfIOs>
    constexpr static GPIO makeInstance() {
        // Check for valid base address
        static_assert((baseAddress % 4) == 0,                                          "The GPIO base port address needs to be 32-bit aligned");
        static_assert((baseAddress >= 0x7000'0000UL) && (baseAddress < 0x8000'2000UL), "The GPIO has to be within the APB bus range");

        // Check for valid number of IOs
        static_assert(numberOfIOs>0 && numberOfIOs<=8,                                 "The number of IOs in this GPIO port_s needs to be between 1-32");

        return GPIO(baseAddress, numberOfIOs);
    }

    template<port_e portId>
    void config(portMode_e mode) {
        static_assert(numberOfIOs > static_cast<uint32_t>(portId), "You are trying to access IOs above your numberOfIos configured in the instance");

        *(volatile uint32_t *)(baseAddress + static_cast<uint32_t>(portId)) = static_cast<uint32_t>(mode);
    }

};

auto gpioOut = GPIO::makeInstance<0x7000'5000UL, 2>();

int main(void) {
    gpioOut.config<GPIO::port_e::io0>(GPIO::portMode_e::output);
    gpioOut.config<GPIO::port_e::io1>(GPIO::portMode_e::output);

    return 0u;
}

所以我想知道是否有办法让命名空间更受保护?就在将结构实例作为 GPIO 实例的句柄返回时,我也在暴露结构类型的过程中。

或者如何正确构建 OOP 方法并仍然具有与名称空间方法类似的 0 运行时开销?

或者是否有我缺少的整个设计模式可以从完全不同的角度解决这个问题?

对于多个问题表示歉意,看起来我没有足够的知识甚至无法提出正确的问题,而且我不想通过要求需要以 OOP 样式完成来强迫某人的手使用不同的编程风格可以轻松实现目标。

我正在使用 C++17,这是我的偏好,但如果这样更容易回答,我可以切换到 C++20。

感谢@Human-Compiler 和@KamilCuk 的评论,这是一种将工厂移至 instance_s 结构并使构造函数私有的解决方案可以使用工厂函数。

https://godbolt.org/z/dcWGK1cxf

#include <cstdint>

namespace GPIO {

    enum class port_e:uint32_t {
        io0  = 0,
        io1  = 1,
        io2  = 2,
        io3  = 3,
        io4  = 4,
        io5  = 5,
        io6  = 6,
        io7  = 7,
    };

    enum class portMode_e:uint32_t {
        input  = 0b10,
        output = 0b01,
    };

    struct instance_s {
        uintptr_t baseAddress;
        uint32_t  numberOfIOs;

        template<uint32_t baseAddress, uint32_t numberOfIOs>
        constexpr static instance_s make() {
            // Check for valid base address
            static_assert((baseAddress % 4) == 0,                                          "The GPIO base port address needs to be 32-bit aligned");
            static_assert((baseAddress >= 0x7000'0000UL) && (baseAddress < 0x8000'2000UL), "The GPIO has to be within the APB bus range");

            // Check for valid number of IOs
            static_assert(numberOfIOs>0 && numberOfIOs<=8,                                 "The number of IOs in this GPIO port_s needs to be between 1-32");

            return {baseAddress, numberOfIOs};
        }

        private:
        constexpr instance_s(uint32_t baseAddress, uint32_t numberOfIOs): 
            baseAddress(baseAddress), numberOfIOs(numberOfIOs) { }
    };

    template<const instance_s* thisGpio, port_e portId>
    void config(portMode_e mode) {
        static_assert(thisGpio->numberOfIOs > static_cast<uint32_t>(portId), "You are trying to access IOs above your numberOfIos configured in the instance");

        *(volatile uint32_t *)(thisGpio->baseAddress + static_cast<uint32_t>(portId)) = static_cast<uint32_t>(mode);
    }

}

constexpr auto gpioOut = GPIO::instance_s::make<0x7000'5000UL, 2>();

// constexpr auto rogueInstance = GPIO::instance_s{0x7000'5000UL, 2};   // will fail because the constructor is private

int main(void) {
    GPIO::config<&gpioOut, GPIO::port_e::io0>(GPIO::portMode_e::output);
    GPIO::config<&gpioOut, GPIO::port_e::io1>(GPIO::portMode_e::output);

    return 0u;
}

想知道我是否应该只是为了练习而尝试 'friend' 方法并尝试使用 OOP 方法。