模板化对象的 C++ 数组:强制转换为通用的非类型模板参数是 UB 吗?

C++ Array of templated objects: Is it UB to cast to a common non-type template parameter?

背景

我正在尝试将 "registers" 的 C 风格 API 现代化为跨嵌入式应用程序的容器(数组)TU 静态变量,以将它们分组到功能相关的组中并在整个应用程序生命周期中跟踪这些组的值。该接口通过标记的 void 指针支持多种变量类型,这需要条件逻辑在需要时转换回正确类型的值(例如用于日志记录)。

编译器:ARM GCC 8.2

完整代码:Compiler explorer

稳定代码

下面是变量 "wrapper" 和这些包装器的集合的示例实施例:

#include <cstdint>
#include <cstring>
#include <array>

enum class VarT {
    kUndefined,
    kBoolean,
    kUinteger
};

/* "Wraps" the desired variable-to-register */
struct Var {
    const VarT val_t;
    const void * const val;
    const char * const name; /* User-facing var name */

    constexpr Var(const char * const name, void* value, VarT type)
        : val_t{type}
        , val{value}
        , name{name}
    { }
};

/* A naive attempt at generalizing the templated Group objects */
struct Base { };

/* A group of `Var`s */
template<std::size_t TNumVars>
struct VarGroup : Base {
    /* Placed first strategically -- shared data */
    std::size_t num_vars = TNumVars;
    /* Because this is "flexible" from VarGroup to VarGroup
     * due to TNumVars template parameter */
    std::array<Var, TNumVars> vars;  

    explicit constexpr VarGroup(std::array<Var, TNumVars> var_arr)
        : vars{var_arr}
    { }
};

有问题的代码

尝试创建 VarGroup 集合:

/* What makes up the collection of VarGroups */
struct GroupHandle {
    /* User-facing VarGroup name */
    const char * name;   
    /* Pointer to statically allocated VarGroup in other TUs */
    const Base * group;  
};

const std::size_t max_groups = 5;

/* The array of VarGroups */
auto groups = std::array<GroupHandle, max_groups>{};
std::size_t groups_idx = 0;

void regGroup(const char * name, const Base * g) {
    /* Ignore bounds checking for the example */
    groups[groups_idx++] = GroupHandle{name, g};
}

/*   THE QUESTIONABLE CAST   */
/*             |             */
/*             v             */
const VarGroup<1>* getGroup(const char * name) {
    for (auto& g : groups) {
        if (std::strcmp(name, g.name) == 0) {
            return static_cast<const VarGroup<1>*>(g.group);
        }
    }
    return nullptr;
}

测试代码

/* Some variables to track */
std::uint32_t uint_var = 42;
bool bool_var = true;

/* Create a group */
constexpr auto group1 = []() {
    std::array vars = {
        Var("g1 uint var", &uint_var, VarT::kUinteger),
        Var("g1 bool var", &bool_var, VarT::kBoolean)
    };
    return VarGroup(vars);
}();

/* Create another group */
constexpr auto group2 = []() {
    std::array vars = {
        Var("g2 uint var", &uint_var, VarT::kUinteger)
    };
    return VarGroup(vars);
}();

/* test */
int main() {
    regGroup("group one", &group1);
    regGroup("group two", &group2);

    /* get group one and iterate over all of its vars */
    auto g1 = getGroup("group one");
    if (g1 != nullptr) {
        printf("Group one vars: \n");
        for (std::size_t i = 0; i < g1->num_vars; i++) {
            auto var = g1->vars[i];
            printf("%s | %d | %d\n", var.name, var.val_t, *((int*)var.val));
        }
    }

    uint_var = 7;

    /* get group two and iterate over all of its vars */
    auto g2 = getGroup("group two");
    if (g2 != nullptr) {
        printf("Group two vars: \n");
        for (std::size_t i = 0; i < g2->num_vars; i++) {
            auto var = g2->vars[i];
            printf("%s | %d | %d\n", var.name, var.val_t, *((int*)var.val));
        }
    }
}

输出:

Group one vars:
g1 uint var | 2 | 42
g1 bool var | 1 | 1
Group two vars:
g2 uint var | 2 | 7

这按预期工作,但肯定感觉很脏。这种类型强制转换的实例——尽管看起来无关紧要,因为静态数组大小分配取决于非类型模板参数——是否导致不安全

基本上您是将 VarGroup<2>* 转换为 VarGroup<1>*,但它们是不同的 类。

VarGroup<x> 不是标准布局,因此您期望通用初始序列起作用 (num_vars) 是无效的。您或许可以将 num_vars 放入 Base 以使其以合法方式可用。您通过错误类型的指针访问 g2->vars 是完全非法的,因为 VarGroup<1>::varsVarGroup<2>::vars 是不同的类型。

底线是您的代码包含 "undefined behavior",它可能看起来有效 今天但明天或在代码或编译器选项稍作更改后停止。

我会做什么:

struct Var {
    const std::variant<std::uint32_t*, bool*> val;
    const char* name; /* User-facing var name */

    constexpr Var(const char* const name, std::variant<std::uint32_t*, bool*> value)
        : val{value}
        , name{name}
    { }
};

struct VarGroup
{
    std::vector<Var> vars;  

    explicit VarGroup(std::vector<Var> var_arr)
        : vars{std::move(var_arr)}
    {}
};

using GroupHandle = std::map<std::string, VarGroup>;

然后测试代码将是

/* Some variables to track */
std::uint32_t uint_var = 42;
bool bool_var = true;

/* Create a group */
constexpr auto group1 = []() {
    std::vector vars = {
        Var("g1 uint var", &uint_var),
        Var("g1 bool var", &bool_var)
    };
    return VarGroup(std::move(vars));
};

/* Create another group */
constexpr auto group2 = []() {
    std::vector vars = {
        Var("g2 uint var", &uint_var)
    };
    return VarGroup(vars);
};

/* test */
int main() {
    GroupHandle groups{
        {"group one", group1()},
        {"group two", group2()}
};

    /* get group one and iterate over all of its vars */
    if (auto it = groups.find("group one"); it != groups.end()) {
        std::cout << "Group one vars: \n";
        for (const auto& var : (*it).second.vars) {

            std::cout << var.name << " | ";
            std::visit([](const auto* p){ std::cout << *p << std::endl; }, var.val);
        }
    }

    uint_var = 7;

    /* get group two and iterate over all of its vars */
    if (auto it = groups.find("group two"); it != groups.end()) {
        std::cout << "Group two vars: \n";
        for (const auto& var : (*it).second.vars) {

            std::cout << var.name << " | ";
            std::visit([](const auto* p){ std::cout << *p << std::endl; }, var.val);
        }
    }
}

Demo

如果您真的想避免使用 std::vector(std::map) 进行动态分配,您可以改用 std::span 并保持查看的数据处于活动状态(静态 constexpr 数组)。