是否可以使用 std::array 作为 POD 结构的数据容器?

Is it possible to use a std::array as a data container for a POD struct?

我正在尝试将一些使用数组作为数据容器并通过宏进行命名访问的遗留 C 代码更新为更优雅的 C++17 解决方案(如果可能,将更新为 C++20欢迎使用 C++20 解决方案)。对不起,如果有很多代码,但这是我的第一个 Whosebug 问题,欢迎提供有关布局的建议。

当前的遗留 C 设计:

#define WORD_ARR_SIZE   100
int16_t         word_arr[WORD_ARR_SIZE];    //two byte variables

#define var0        word_arr[0] //macros are used to hide the array and use its members like variables
#define myValue     word_arr[1]
#define myParameter word_arr[2]
#define free        ((uint16_t)word_arr[3]) //'hidden' explicit cast needed when using the array as all data must be of the same type
#define var1        word_arr[4]
#define var2        word_arr[5]
#define var3        word_arr[6]
#define var4        word_arr[7]
#define var5        word_arr[7] //very easy to write the wrong index when adding new 'variables'

extern int send(int16_t* arr, size_t size); //The array is necessary as it needs to be fed to a library (code cannot be modified)

int main()
{
    (uint16_t)var1 = UINT16_MAX; //'visible' explicit cast needed when using the array as all data is of the same type
    myValues = 50;

    for(int a = 20; a < 30; a++)
    {
        word_arr[a] = 10;   //array is also used like it should be
    }

    return send(word_arr, WORD_ARR_SIZE);
}

我第一次尝试解决这个问题是使用结构而不是数组,这消除了显式转换和对宏的需要,但缺点是缺少数组实现具有的通过索引的简单访问,替换它有一个丑陋的 reinterpret_cast.

//no need for pragma pack, the code doesn't care about padding
struct word_arr_t
{
    int16_t var0;   //no more macros
    int16_t myValue;    
    int16_t myParameter;
    uint16_t free;   //no need for cast, value is alredy declared using the correct type
    int16_t var1;       
    int16_t var2;  //no way to get the index as if it was an array by simply using the value.
    int16_t var3;       
    int16_t var4;       
    int16_t var5;       
}word_arr;

constexpr size_t WORD_ARR_SIZE = sizeof(word_arr_t) / sizeof(uint16_t);

auto word_arr_p = reinterpret_cast<int16_t*>(&word_arr); //needed for indexed access

extern int send(int16_t* arr, size_t size);

int main()
{
    word_arr.var1 = UINT16_MAX;
    word_arr.myValues = 50;

    for(int a = 20; a < 30; a++)
    {
        word_arr_p[a] = 10;   //'hidden' pointer arithmetic to access the struct like an array
    }

    return send(word_arr_p, sizeof(word_arr_t));
}

目前的解决方案: 我创建了一个名为 SmartStruct 的自定义模板 class,我在模板中传递了结构类型和值类型;我为 operator[] 创建了一个重载,允许通过隐藏丑陋的 reinterpret_cast;

的索引进行访问
/**
 * \brief   A wrapper for structs made of object of the same type, allows indexed access
 * \tparam StructT  struct type
 * \tparam DataT    struct data type
 */
template <typename StructT, typename DataT>
class SmartStruct
{
    DataT* m_dataPointer;
public:
    /**
     * \brief let the struct be accessible from the outside as well
     */
    StructT Data;
    const size_t Count;

    /**
     * \brief Default constructor
     */
    SmartStruct();

    /**
     * \brief Construct by struct copy
     * \param data struct to copy
     */
    explicit SmartStruct(const StructT& data);

    /**
     * \brief operator to access struct in array style 
     * \param index element to access
     * \return element, if index >= size then first element
     */
    DataT& operator[](size_t index);
};

template <typename StructT, typename DataT>
SmartStruct<StructT, DataT>::SmartStruct() : Data{}, Count{ sizeof Data / sizeof(DataT) }
{
    m_dataPointer = reinterpret_cast<DataT*>(&Data);
}

template <typename StructT, typename DataT>
SmartStruct<StructT, DataT>::SmartStruct(const StructT& data) : Count{ sizeof data / sizeof(DataT) }
{
    //copy the struct
    Data = data;
    m_dataPointer = reinterpret_cast<DataT*>(&Data);
}

template <typename StructT, typename DataT>
DataT& SmartStruct<StructT, DataT>::operator[](size_t index)
{
    if (index >= Count)
    {
        return *m_dataPointer;
    }

    return m_dataPointer[index];
}

用法示例:

struct word_arr_t
{
    int16_t var0;
    int16_t myValue;
    int16_t myParameter;
    uint16_t free;
    int16_t var1;
    int16_t var2;
    int16_t var3; //Still no way to get array index from variable name
    int16_t var4;
    int16_t var5;
};

SmartStruct<word_arr_t, word> smart_word_arr{}; //Would love it if I could use std containers interface without having to implement it all by hand...


extern int send(int16_t* arr, size_t size);

int main()
{
    word_arr_t& word_arr = smart_word_arr.Data;

    word_arr.var1 = UINT16_MAX;
    word_arr.myValues = 50;

    for(int a = 20; a < 30; a++)
    {
        smart_word_arr[a] = 10;
    }

    return send(&smart_word_arr[0], smart_word_arr.Count);
}

既然我了解了上下文,我终于可以开始真正的问题了:

是否可以使用 std::array 作为结构的数据容器? 意思是通过结构初始化它;这将使使用结构本身通过变量访问数据以及使用 std::array 通过索引访问数据成为可能,并且具有标准接口的额外好处,而无需重新实现它。

我目前尝试让这个解决方案起作用:

struct word_arr_t
{
    int16_t var0;
    int16_t myValue;
    int16_t myParameter;
    uint16_t free;
    int16_t var1;
    int16_t var2;
    int16_t var3; //Still no way to get array index from variable name
    int16_t var4;
    int16_t var5;
}word_struct;

std:.array<int16_t, sizeof(word_arr_t) / sizeof(word)> word_array{};
//std:.array<int16_t, sizeof(word_arr_t) / sizeof(word)> word_array{&word_struct}; would be lovely if I could do this.
//word_array.Data = reinterpret_cast<int16_t*>(&word_struct); this would also be good.

extern int send(int16_t* arr, size_t size);

int main()
{
    word_struct.var1 = UINT16_MAX;
    word_struct.myValues = 50;

    //copy struct into array, very very bad as it's not usable unless you know when
    //code writes to the struct and when code writes to the array,
    //this could be solved by wrapping the array into a read only object but still not ideal
    //and extremely slow especially if the struct is very large
    memcpy(word_array.Data, &word_struct, sizeof(word_struct));

    for(auto& a : word_array)
    {
        a = 10;
    }

    return send(word_array.Data, word_array.Size);
}

您不能将单独的变量视为数组。

不理想,但您可以使用访问器,例如:

struct word_arr_t
{
    std::array<std::int16_t, 100> data{}; // your array

    int16_t& var0() { return data[0]; }
    int16_t& myValue()  { return data[1]; }    
    int16_t& myParameter { return data[2]; }
    uint16_t free() const { return static_cast<uint16_t>(data[3]); }
    void set_free(uint16_t value) { data[3] = static_cast<int16_t>(value); }
    int16_t& var1() { return data[4]; }
    int16_t& var2() { return data[5]; }
    int16_t& var3() { return data[6]; }
    int16_t& var4() { return data[7]; }
    int16_t& var5() { return data[8]; }
};

int main()
{
    word_arr_t word_arr;
    word_arr.var1() = INT16_MAX;
    word_arr.myValues() = 50;

    send(word_arr.data.data(), word_arr.data.size());
    // ...
}

或枚举索引:

enum Index
{
    var0,
    myValue,
    myParameter,
    free,
    var1,
    var2,
    var3,
    var4,
    var5,
};

struct word_arr_t
{
    std::array<std::int16_t, 100> data{}; // your array
};


int main()
{
    word_arr_t word_arr;
    word_arr.data[Index::var1] = INT16_MAX;
    word_arr.data[Index::myValues] = 50;

    send(word_arr.data.data(), word_arr.data.size());
    // ...
}

Would it be possible to use a std::array as a a data container for the struct?

一个POD struct in C++ is a class that is trivial and has standard layout.
std::array 作为 "struct" 的底层存储意味着您实际上并没有定义任何具有命名成员的 struct。您将不得不编写一个 class,其中包含数组和访问数组索引的成员,并且很容易成为 不平凡 非标准布局.

通过重载 operator[],您可以使用类似数组的语法访问结构的 "array" 成员,同时保持 struct 定义和 POD 状态。

如果它 看起来 struct[]std::array,那么它可能 不错就最终用户而言,足够

这是一个示例程序,演示了:

#include <iostream>
#include <stdexcept>
#include <type_traits>

struct ArrPODStruct
{
    int someHeaderValue1;
    int someHeaderValue2;
    int someHeaderValue3;

    int a0;
    int a1;
    int a2;
    int a3;
    int a4;

    int operator[] (int i)
    {
        switch (i)
        {
            case 0: return a0;
            case 1: return a1;
            case 2: return a2;
            case 3: return a3;
            case 4: return a4;
            default: throw std::out_of_range("...");
        }
    }
};

int main()
{
    ArrPODStruct arr;
    // Put some useful data in arr...

    std::cout << "Is POD: " << (std::is_pod<ArrPODStruct>() ? "Yes" : "No") << "\n";
    std::cout << "Element 0: " << arr.a0 << " or " << arr[0] << "\n";
    std::cout << "Element 1: " << arr.a1 << " or " << arr[1] << "\n";
    std::cout << "Element 2: " << arr.a2 << " or " << arr[2] << "\n";
    std::cout << "Element 3: " << arr.a3 << " or " << arr[3] << "\n";
    std::cout << "Element 4: " << arr.a4 << " or " << arr[4] << "\n";
}

输出:

Is POD: Yes
Element 0: 0 or 0
Element 1: 0 or 0
Element 2: 0 or 0
Element 3: 0 or 0
Element 4: 0 or 0

如果可以选择将这部分代码编译为 C,那么目前为止最优雅的解决方案是使用 union。由于对齐在这里不是问题,因此不会有填充。这只能在 C 中完成,它允许通过联合进行类型双关。

typedef union
{
  struct // C11 anonymous struct
  {
    int16_t  var0;
    int16_t  myValue;
    int16_t  myParameter,
    uint16_t free;
    int16_t  var1;
    int16_t  var2;
    int16_t  var3;
    int16_t  var4;
    // var5 not declared on purpose
  };

  int16_t arr [WORD_ARR_SIZE];

} word_t;

原来的C代码应该是这样写的。这里所有的类型信息和特殊情况都在编译时处理,从 int16_tuint16_t 的类型双关是定义明确的,它们的有效类型别名。

您可以为特殊情况 var5 建立一个 enum 索引:

typedef enum
{
  var0_index = 0,
  myValue_index = 1,
  myParameter_index = 2,
  free_index = 3,
  var_1index = 4,
  var_2index = 5,
  var_3index = 6,
  var_4index = 7,
  var_5index = 7,
} var_t;

word_t word = 
{
  // different sorts of designated initializers can be mixed:
    .var0 = x,
  .myValue = y,      
  [var5_index] = z,
};

("word" 是一个可怕的 type/variable 名字,因为在计算机科学中术语词指的是一个完整的整数类型。所以请想出更好的名字。)