【问题标题】:C++ Array of templated objects: Is it UB to cast to a common non-type template parameter?C ++模板对象数组:转换为常见的非类型模板参数是HUB吗?
【发布时间】:2019-08-01 16:09:59
【问题描述】:

背景

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

编译器:ARM GCC 8.2

完整代码:Compiler explorer

稳定的代码

以下是变量“包装器”和这些包装器的集合的示例实施例:

#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}
    { }
};

可疑代码

尝试创建 VarGroups 的集合:

/* 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 中使用std::vector 而不是std::array(因此删除模板),那么您将拥有相同的类型并且能够拥有std::vector&lt;VarGroup&gt;
  • 如果你的类没有共同点,它们就不应该有一个共同的基类。如果他们确实有一些共同点,那么这些东西应该在他们的共同基类中。空基类基本上是一个错误。

标签: c++ templates casting containers undefined-behavior


【解决方案1】:

基本上,您将VarGroup&lt;2&gt;* 转换为VarGroup&lt;1&gt;*,但它们是不同的类。

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

底线是您的代码包含“未定义的行为”,它可能看起来有效 今天,但明天或在代码或编译器选项略有变化后停止。

【讨论】:

    【解决方案2】:

    我会做什么:

    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 数组)。

    【讨论】:

    • 虽然这是一个优雅的完整 STL 解决方案,但不幸的是,它产生的二进制文件比所讨论的代码大 10 倍左右,特别是考虑到这是针对嵌入式固件应用程序的情况。我通过将GroupHandle 转变为更多类似于string_view 的实体来解决我的设计问题,它向给定Var 数组中的Var 数组公开了一个简单的迭代器接口,并回避了对各种处理的需要。 VarGroup&lt;N&gt; 个实例。
    猜你喜欢
    • 1970-01-01
    • 2012-08-13
    • 2021-09-02
    • 1970-01-01
    • 2021-03-18
    • 1970-01-01
    • 1970-01-01
    • 2014-10-03
    • 1970-01-01
    相关资源
    最近更新 更多